mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-19 19:08:11 +08:00
446 lines
13 KiB
JavaScript
446 lines
13 KiB
JavaScript
/* eslint-disable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
|
|
import { strict as assert } from "assert";
|
|
import {
|
|
clearInputAndTypeInto,
|
|
clickOn,
|
|
getAllByClass,
|
|
getOneElementInnerText,
|
|
typeInto,
|
|
waitByID,
|
|
waitByClass,
|
|
waitForAllByIds,
|
|
clickOnUntil,
|
|
getTestClass,
|
|
getTestId,
|
|
isElementPresent,
|
|
} from "./puppeteerUtils";
|
|
|
|
export async function drag(testId, start, end, lasso = false) {
|
|
const layout = await waitByID(testId);
|
|
const elBox = await layout.boxModel();
|
|
const x1 = elBox.content[0].x + start.x;
|
|
const x2 = elBox.content[0].x + end.x;
|
|
const y1 = elBox.content[0].y + start.y;
|
|
const y2 = elBox.content[0].y + end.y;
|
|
await page.mouse.move(x1, y1);
|
|
await page.mouse.down();
|
|
if (lasso) {
|
|
await page.mouse.move(x2, y1);
|
|
await page.mouse.move(x2, y2);
|
|
await page.mouse.move(x1, y2);
|
|
await page.mouse.move(x1, y1);
|
|
} else {
|
|
await page.mouse.move(x2, y2);
|
|
}
|
|
await page.mouse.up();
|
|
}
|
|
|
|
export async function clickOnCoordinate(testId, coord) {
|
|
const layout = await expect(page).toMatchElement(getTestId(testId));
|
|
const elBox = await layout.boxModel();
|
|
|
|
if (!elBox) {
|
|
throw Error("Layout's boxModel is not available!");
|
|
}
|
|
|
|
const x = elBox.content[0].x + coord.x;
|
|
const y = elBox.content[0].y + coord.y;
|
|
await page.mouse.click(x, y);
|
|
}
|
|
|
|
export async function getAllHistograms(testclass, testIds) {
|
|
const histTestIds = testIds.map((tid) => `histogram-${tid}`);
|
|
|
|
// these load asynchronously, so we need to wait for each histogram individually,
|
|
// and they may be quite slow in some cases.
|
|
await waitForAllByIds(histTestIds, { timeout: 4 * 60 * 1000 });
|
|
|
|
const allHistograms = await getAllByClass(testclass);
|
|
|
|
const testIDs = await Promise.all(
|
|
allHistograms.map((hist) =>
|
|
page.evaluate((elem) => elem.dataset.testid, hist)
|
|
)
|
|
);
|
|
|
|
return testIDs.map((id) => id.replace(/^histogram-/, ""));
|
|
}
|
|
|
|
export async function getAllCategoriesAndCounts(category) {
|
|
// these load asynchronously, so we have to wait for the specific category.
|
|
await waitByID(`category-${category}`);
|
|
|
|
return page.$$eval(
|
|
`[data-testid="category-${category}"] [data-testclass='categorical-row']`,
|
|
(rows) =>
|
|
Object.fromEntries(
|
|
rows.map((row) => {
|
|
const cat = row
|
|
.querySelector("[data-testclass='categorical-value']")
|
|
.getAttribute("aria-label");
|
|
|
|
const count = row.querySelector(
|
|
"[data-testclass='categorical-value-count']"
|
|
).innerText;
|
|
|
|
return [cat, count];
|
|
})
|
|
)
|
|
);
|
|
}
|
|
|
|
export async function getCellSetCount(num) {
|
|
await clickOn(`cellset-button-${num}`);
|
|
return getOneElementInnerText(`[data-testid='cellset-count-${num}']`);
|
|
}
|
|
|
|
export async function resetCategory(category) {
|
|
const checkboxId = `${category}:category-select`;
|
|
await waitByID(checkboxId);
|
|
const checkedPseudoclass = await page.$eval(
|
|
`[data-testid='${checkboxId}']`,
|
|
(el) => el.matches(":checked")
|
|
);
|
|
if (!checkedPseudoclass) await clickOn(checkboxId);
|
|
|
|
const categoryRow = await waitByID(`${category}:category-expand`);
|
|
|
|
const isExpanded = await categoryRow.$(
|
|
"[data-testclass='category-expand-is-expanded']"
|
|
);
|
|
|
|
if (isExpanded) await clickOn(`${category}:category-expand`);
|
|
}
|
|
|
|
export async function calcCoordinate(testId, xAsPercent, yAsPercent) {
|
|
const el = await waitByID(testId);
|
|
const size = await el.boxModel();
|
|
return {
|
|
x: Math.floor(size.width * xAsPercent),
|
|
y: Math.floor(size.height * yAsPercent),
|
|
};
|
|
}
|
|
|
|
export async function calcDragCoordinates(testId, coordinateAsPercent) {
|
|
return {
|
|
start: await calcCoordinate(
|
|
testId,
|
|
coordinateAsPercent.x1,
|
|
coordinateAsPercent.y1
|
|
),
|
|
end: await calcCoordinate(
|
|
testId,
|
|
coordinateAsPercent.x2,
|
|
coordinateAsPercent.y2
|
|
),
|
|
};
|
|
}
|
|
|
|
export async function selectCategory(category, values, reset = true) {
|
|
if (reset) await resetCategory(category);
|
|
|
|
await clickOn(`${category}:category-expand`);
|
|
await clickOn(`${category}:category-select`);
|
|
|
|
for (const value of values) {
|
|
await clickOn(`categorical-value-select-${category}-${value}`);
|
|
}
|
|
}
|
|
|
|
export async function expandCategory(category) {
|
|
const expand = await waitByID(`${category}:category-expand`);
|
|
const notExpanded = await expand.$(
|
|
"[data-testclass='category-expand-is-not-expanded']"
|
|
);
|
|
if (notExpanded) await clickOn(`${category}:category-expand`);
|
|
}
|
|
|
|
export async function clip(min = 0, max = 100) {
|
|
await clickOn("visualization-settings");
|
|
await clearInputAndTypeInto("clip-min-input", min);
|
|
await clearInputAndTypeInto("clip-max-input", max);
|
|
await clickOn("clip-commit");
|
|
}
|
|
|
|
export async function createCategory(categoryName) {
|
|
await clickOnUntil("open-annotation-dialog", async () => {
|
|
await expect(page).toMatchElement(getTestId("new-category-name"));
|
|
});
|
|
|
|
await typeInto("new-category-name", categoryName);
|
|
await clickOn("submit-category");
|
|
}
|
|
|
|
/*
|
|
|
|
GENESET
|
|
|
|
*/
|
|
|
|
export async function colorByGeneset(genesetName) {
|
|
await clickOn(`${genesetName}:colorby-entire-geneset`);
|
|
}
|
|
|
|
export async function colorByGene(gene) {
|
|
await clickOn(`colorby-${gene}`);
|
|
}
|
|
|
|
export async function assertColorLegendLabel(label) {
|
|
const handle = await waitByID("continuous_legend_color_by_label");
|
|
|
|
const result = await handle.evaluate((node) =>
|
|
node.getAttribute("aria-label")
|
|
);
|
|
|
|
return expect(result).toBe(label);
|
|
}
|
|
|
|
export async function expandGeneset(genesetName) {
|
|
const expand = await waitByID(`${genesetName}:geneset-expand`);
|
|
const notExpanded = await expand.$(
|
|
"[data-testclass='geneset-expand-is-not-expanded']"
|
|
);
|
|
if (notExpanded) await clickOn(`${genesetName}:geneset-expand`);
|
|
}
|
|
|
|
export async function createGeneset(genesetName) {
|
|
await clickOnUntil("open-create-geneset-dialog", async () => {
|
|
await expect(page).toMatchElement(getTestId("create-geneset-input"));
|
|
});
|
|
|
|
await typeInto("create-geneset-input", genesetName);
|
|
await clickOn("submit-geneset");
|
|
await waitByClass("autosave-complete");
|
|
}
|
|
|
|
export async function editGenesetName(genesetName, editText) {
|
|
const editButton = `${genesetName}:edit-genesetName-mode`;
|
|
const submitButton = `${genesetName}:submit-geneset`;
|
|
await clickOnUntil(`${genesetName}:see-actions`, async () => {
|
|
await expect(page).toMatchElement(getTestId(editButton));
|
|
});
|
|
await clickOn(editButton);
|
|
await typeInto("rename-geneset-modal", editText);
|
|
await clickOn(submitButton);
|
|
}
|
|
|
|
export async function deleteGeneset(genesetName) {
|
|
const targetId = `${genesetName}:delete-geneset`;
|
|
|
|
await clickOnUntil(`${genesetName}:see-actions`, async () => {
|
|
await expect(page).toMatchElement(getTestId(targetId));
|
|
});
|
|
|
|
await clickOn(targetId);
|
|
|
|
await assertGenesetDoesNotExist(genesetName);
|
|
await waitByClass("autosave-complete");
|
|
}
|
|
|
|
export async function assertGenesetDoesNotExist(genesetName) {
|
|
const result = await isElementPresent(
|
|
getTestId(`${genesetName}:geneset-name`)
|
|
);
|
|
await expect(result).toBe(false);
|
|
}
|
|
|
|
export async function assertGenesetExists(genesetName) {
|
|
const handle = await waitByID(`${genesetName}:geneset-name`);
|
|
|
|
const result = await handle.evaluate((node) =>
|
|
node.getAttribute("aria-label")
|
|
);
|
|
|
|
return expect(result).toBe(genesetName);
|
|
}
|
|
|
|
/*
|
|
|
|
GENE
|
|
|
|
*/
|
|
|
|
export async function addGeneToSet(genesetName, geneToAddToSet) {
|
|
const submitButton = `${genesetName}:submit-gene`;
|
|
|
|
await clickOn(`${genesetName}:add-new-gene-to-geneset`);
|
|
await typeInto("add-genes", geneToAddToSet);
|
|
await clickOn(submitButton);
|
|
}
|
|
|
|
export async function removeGene(geneSymbol) {
|
|
const targetId = `delete-from-geneset:${geneSymbol}`;
|
|
|
|
await clickOn(targetId);
|
|
|
|
await waitByClass("autosave-complete");
|
|
}
|
|
|
|
export async function assertGeneExistsInGeneset(geneSymbol) {
|
|
const handle = await waitByID(`${geneSymbol}:gene-label`);
|
|
|
|
const result = await handle.evaluate((node) =>
|
|
node.getAttribute("aria-label")
|
|
);
|
|
|
|
return expect(result).toBe(geneSymbol);
|
|
}
|
|
|
|
export async function assertGeneDoesNotExist(geneSymbol) {
|
|
const result = await isElementPresent(getTestId(`${geneSymbol}:gene-label`));
|
|
|
|
await expect(result).toBe(false);
|
|
}
|
|
|
|
export async function expandGene(geneSymbol) {
|
|
await clickOn(`maximize-${geneSymbol}`);
|
|
}
|
|
|
|
/*
|
|
|
|
CATEGORY
|
|
|
|
*/
|
|
|
|
export async function duplicateCategory(categoryName) {
|
|
await clickOn("open-annotation-dialog");
|
|
|
|
await typeInto("new-category-name", categoryName);
|
|
|
|
const dropdownOptionClass = "duplicate-category-dropdown-option";
|
|
|
|
await clickOnUntil("duplicate-category-dropdown", async () => {
|
|
await expect(page).toMatchElement(getTestClass(dropdownOptionClass));
|
|
});
|
|
|
|
const option = await expect(page).toMatchElement(
|
|
getTestClass(dropdownOptionClass)
|
|
);
|
|
|
|
await option.click();
|
|
|
|
await clickOnUntil("submit-category", async () => {
|
|
await expect(page).toMatchElement(
|
|
getTestId(`${categoryName}:category-expand`)
|
|
);
|
|
});
|
|
|
|
await waitByClass("autosave-complete");
|
|
}
|
|
|
|
export async function renameCategory(oldCategoryName, newCategoryName) {
|
|
await clickOn(`${oldCategoryName}:see-actions`);
|
|
await clickOn(`${oldCategoryName}:edit-category-mode`);
|
|
await clearInputAndTypeInto(
|
|
`${oldCategoryName}:edit-category-name-text`,
|
|
newCategoryName
|
|
);
|
|
await clickOn(`${oldCategoryName}:submit-category-edit`);
|
|
}
|
|
|
|
export async function deleteCategory(categoryName) {
|
|
const targetId = `${categoryName}:delete-category`;
|
|
|
|
await clickOnUntil(`${categoryName}:see-actions`, async () => {
|
|
await expect(page).toMatchElement(getTestId(targetId));
|
|
});
|
|
|
|
await clickOn(targetId);
|
|
|
|
await assertCategoryDoesNotExist();
|
|
}
|
|
|
|
export async function createLabel(categoryName, labelName) {
|
|
/**
|
|
* (thuang): This explicit wait is needed, since currently showing
|
|
* the modal again quickly after the previous action dismissing the
|
|
* modal will persist the input value from the previous action.
|
|
*
|
|
* To reproduce:
|
|
* 1. Click on the plus sign to show the modal to add a new label to the category
|
|
* 2. Type `123` in the input box
|
|
* 3. Hover over your mouse over the plus sign and double click to quickly dismiss and
|
|
* invoke the modal again
|
|
* 4. You will see `123` is persisted in the input box
|
|
* 5. Expected behavior is to get an empty input box
|
|
*/
|
|
await page.waitForTimeout(500);
|
|
|
|
await clickOn(`${categoryName}:see-actions`);
|
|
|
|
await clickOn(`${categoryName}:add-new-label-to-category`);
|
|
|
|
await typeInto(`${categoryName}:new-label-name`, labelName);
|
|
|
|
await clickOn(`${categoryName}:submit-label`);
|
|
}
|
|
|
|
export async function deleteLabel(categoryName, labelName) {
|
|
await expandCategory(categoryName);
|
|
await clickOn(`${categoryName}:${labelName}:see-actions`);
|
|
await clickOn(`${categoryName}:${labelName}:delete-label`);
|
|
}
|
|
|
|
export async function renameLabel(categoryName, oldLabelName, newLabelName) {
|
|
await expandCategory(categoryName);
|
|
await clickOn(`${categoryName}:${oldLabelName}:see-actions`);
|
|
await clickOn(`${categoryName}:${oldLabelName}:edit-label`);
|
|
await clearInputAndTypeInto(
|
|
`${categoryName}:${oldLabelName}:edit-label-name`,
|
|
newLabelName
|
|
);
|
|
await clickOn(`${categoryName}:${oldLabelName}:submit-label-edit`);
|
|
}
|
|
|
|
export async function addGeneToSearch(geneName) {
|
|
await typeInto("gene-search", geneName);
|
|
await page.keyboard.press("Enter");
|
|
await page.waitForSelector(`[data-testid='histogram-${geneName}']`);
|
|
}
|
|
|
|
export async function subset(coordinatesAsPercent) {
|
|
// In order to deselect the selection after the subset, make sure we have some clear part
|
|
// of the scatterplot we can click on
|
|
assert(coordinatesAsPercent.x2 < 0.99 || coordinatesAsPercent.y2 < 0.99);
|
|
const lassoSelection = await calcDragCoordinates(
|
|
"layout-graph",
|
|
coordinatesAsPercent
|
|
);
|
|
await drag("layout-graph", lassoSelection.start, lassoSelection.end, true);
|
|
await clickOn("subset-button");
|
|
const clearCoordinate = await calcCoordinate("layout-graph", 0.5, 0.99);
|
|
await clickOnCoordinate("layout-graph", clearCoordinate);
|
|
}
|
|
|
|
export async function setSellSet(cellSet, cellSetNum) {
|
|
const selections = cellSet.filter((sel) => sel.kind === "categorical");
|
|
|
|
for (const selection of selections) {
|
|
await selectCategory(selection.metadata, selection.values, true);
|
|
}
|
|
|
|
await getCellSetCount(cellSetNum);
|
|
}
|
|
|
|
export async function runDiffExp(cellSet1, cellSet2) {
|
|
await setSellSet(cellSet1, 1);
|
|
await setSellSet(cellSet2, 2);
|
|
await clickOn("diffexp-button");
|
|
}
|
|
|
|
export async function bulkAddGenes(geneNames) {
|
|
await clickOn("section-bulk-add");
|
|
await typeInto("input-bulk-add", geneNames.join(","));
|
|
await page.keyboard.press("Enter");
|
|
}
|
|
|
|
export async function assertCategoryDoesNotExist(categoryName) {
|
|
const result = await isElementPresent(
|
|
getTestId(`${categoryName}:category-label`)
|
|
);
|
|
|
|
await expect(result).toBe(false);
|
|
}
|
|
|
|
/* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
|