mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-29 09:08:11 +08:00
* Add undo/redo tests for annotations Fixes https://github.com/chanzuckerberg/cellxgene/issues/969 ... also refactor the tests for DRY. * Add done() * Make e2e annotations tests safer to concurrency * Add data-testclass for save state. * Simplify tests and make them dependent on save state
304 lines
10 KiB
JavaScript
304 lines
10 KiB
JavaScript
import {DEBUG, DEV} from "./config";
|
|
import puppeteer from "puppeteer";
|
|
|
|
export const puppeteerUtils = puppeteerPage => ({
|
|
async waitByID(testid, props = {}) {
|
|
return await puppeteerPage.waitForSelector(
|
|
`[data-testid='${testid}']`,
|
|
props
|
|
);
|
|
},
|
|
|
|
async waitByClass(testclass, props = {}) {
|
|
return await puppeteerPage.waitForSelector(
|
|
`[data-testclass='${testclass}']`,
|
|
props
|
|
);
|
|
},
|
|
|
|
async waitForAllByIds(testids, props = {}) {
|
|
await Promise.all(
|
|
testids.map(testid =>
|
|
puppeteerPage.waitForSelector(`[data-testid='${testid}']`)
|
|
)
|
|
);
|
|
},
|
|
|
|
async getAllByClass(testclass, props = {}) {
|
|
const elements = await puppeteerPage.$$eval(
|
|
`[data-testclass=${testclass}]`,
|
|
els => {
|
|
return els.map(el => {
|
|
return el.dataset.testid;
|
|
});
|
|
}
|
|
);
|
|
return elements;
|
|
},
|
|
|
|
async typeInto(testid, text) {
|
|
// only works for text without special characters
|
|
await this.waitByID(testid);
|
|
const selector = `[data-testid='${testid}']`;
|
|
// type ahead can be annoying if you don't pause before you type
|
|
await puppeteerPage.click(selector);
|
|
await puppeteerPage.waitFor(200);
|
|
await puppeteerPage.type(selector, text);
|
|
},
|
|
|
|
async clearInputAndTypeInto(testid, text) {
|
|
await this.waitByID(testid);
|
|
const selector = `[data-testid='${testid}']`;
|
|
// only works for text without special characters
|
|
// type ahead can be annoying if you don't pause before you type
|
|
await puppeteerPage.click(selector);
|
|
await puppeteerPage.waitFor(200);
|
|
// select all
|
|
|
|
await puppeteerPage.click(selector, { clickCount: 3 });
|
|
await puppeteerPage.keyboard.press("Backspace");
|
|
await puppeteerPage.type(selector, text);
|
|
},
|
|
|
|
async clickOn(testid, options={}) {
|
|
await this.waitByID(testid);
|
|
await puppeteerPage.click(`[data-testid='${testid}']`, options);
|
|
await puppeteerPage.waitFor(50);
|
|
},
|
|
|
|
async getOneElementInnerHTML(selector) {
|
|
await puppeteerPage.waitForSelector(selector);
|
|
let text = await puppeteerPage.$eval(selector, el => el.innerHTML);
|
|
return text;
|
|
},
|
|
|
|
async getOneElementInnerText(selector) {
|
|
await puppeteerPage.waitForSelector(selector);
|
|
let text = await puppeteerPage.$eval(selector, el => el.innerText);
|
|
return text;
|
|
}
|
|
});
|
|
|
|
export const cellxgeneActions = puppeteerPage => ({
|
|
async drag(testid, start, end, lasso = false) {
|
|
const layout = await puppeteerUtils(puppeteerPage).waitByID(testid);
|
|
const elBox = await layout.boxModel();
|
|
const x1 = elBox.content[0].x + start.x;
|
|
const x2 = elBox.content[0].x + end.x;
|
|
const y1 = elBox.content[0].y + start.y;
|
|
const y2 = elBox.content[0].y + end.y;
|
|
await puppeteerPage.mouse.move(x1, y1);
|
|
await puppeteerPage.mouse.down();
|
|
if (lasso) {
|
|
await puppeteerPage.mouse.move(x2, y1);
|
|
await puppeteerPage.mouse.move(x2, y2);
|
|
await puppeteerPage.mouse.move(x1, y2);
|
|
await puppeteerPage.mouse.move(x1, y1);
|
|
} else {
|
|
await puppeteerPage.mouse.move(x2, y2);
|
|
}
|
|
await puppeteerPage.mouse.up();
|
|
},
|
|
|
|
async 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
|
|
await puppeteerUtils(puppeteerPage).waitForAllByIds(histTestIds);
|
|
const allHistograms = await puppeteerUtils(puppeteerPage).getAllByClass(
|
|
testclass
|
|
);
|
|
return allHistograms.map(hist =>
|
|
hist.substr("histogram_".length, hist.length)
|
|
);
|
|
},
|
|
|
|
async getAllCategoriesAndCounts(category) {
|
|
await puppeteerUtils(puppeteerPage).waitByClass("categorical-row");
|
|
const categories = await puppeteerPage.$$eval(
|
|
`[data-testid="category-${category}"] [data-testclass='categorical-row']`,
|
|
els => {
|
|
let result = {};
|
|
els.forEach(el => {
|
|
const cat = el.querySelector("[data-testclass='categorical-value']")
|
|
.innerText;
|
|
const count = el.querySelector(
|
|
"[data-testclass='categorical-value-count']"
|
|
).innerText;
|
|
result[cat] = count;
|
|
});
|
|
return result;
|
|
}
|
|
);
|
|
return categories;
|
|
},
|
|
|
|
async cellSet(num) {
|
|
await puppeteerUtils(puppeteerPage).clickOn(`cellset-button-${num}`);
|
|
return await puppeteerUtils(puppeteerPage).getOneElementInnerText(
|
|
`[data-testid='cellset-count-${num}']`
|
|
);
|
|
},
|
|
|
|
async resetCategory(category) {
|
|
const checkboxId = `${category}:category-select`;
|
|
await puppeteerUtils(puppeteerPage).waitByID(checkboxId);
|
|
const checkedPseudoclass = await puppeteerPage.$eval(
|
|
`[data-testid='${checkboxId}']`,
|
|
el => {
|
|
return el.matches(":checked");
|
|
}
|
|
);
|
|
if (!checkedPseudoclass) {
|
|
await puppeteerUtils(puppeteerPage).clickOn(checkboxId);
|
|
}
|
|
try {
|
|
const categoryRow = await puppeteerUtils(puppeteerPage).waitByID(
|
|
`${category}:category-expand`
|
|
);
|
|
const isExpanded = await categoryRow.$(
|
|
"[data-testclass='category-expand-is-expanded']"
|
|
);
|
|
if (isExpanded) {
|
|
await puppeteerUtils(puppeteerPage).clickOn(
|
|
`${category}:category-expand`
|
|
);
|
|
}
|
|
} catch {}
|
|
},
|
|
|
|
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: 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}:category-expand`);
|
|
await puppeteerUtils(puppeteerPage).clickOn(`${category}:category-select`);
|
|
for (const val of values) {
|
|
await puppeteerUtils(puppeteerPage).clickOn(
|
|
`categorical-value-select-${category}-${val}`
|
|
);
|
|
}
|
|
},
|
|
|
|
async expandCategory(category) {
|
|
const expand = await puppeteerUtils(puppeteerPage).waitByID(`${category}:category-expand`);
|
|
const notExpanded = await expand.$("[data-testclass='category-expand-is-not-expanded']");
|
|
if (notExpanded) {
|
|
await puppeteerUtils(puppeteerPage).clickOn(`${category}:category-expand`);
|
|
}
|
|
},
|
|
|
|
async reset() {
|
|
await puppeteerUtils(puppeteerPage).clickOn("reset");
|
|
// loading state never actually happens, reset is too fast
|
|
await page.waitFor(200);
|
|
},
|
|
|
|
async clip(min = 0, max = 100) {
|
|
await puppeteerUtils(puppeteerPage).clickOn("visualization-settings");
|
|
await puppeteerUtils(puppeteerPage).clearInputAndTypeInto(
|
|
"clip-min-input",
|
|
min
|
|
);
|
|
await puppeteerUtils(puppeteerPage).clearInputAndTypeInto(
|
|
"clip-max-input",
|
|
max
|
|
);
|
|
await puppeteerUtils(puppeteerPage).clickOn("clip-commit");
|
|
},
|
|
|
|
async createCategory(categoryName) {
|
|
await puppeteerUtils(puppeteerPage).clickOn("open-annotation-dialog");
|
|
await puppeteerUtils(puppeteerPage).typeInto("new-category-name", categoryName);
|
|
await puppeteerUtils(puppeteerPage).clickOn("submit-category");
|
|
},
|
|
|
|
async renameCategory(oldCatgoryName, newCategoryName) {
|
|
await puppeteerUtils(puppeteerPage).clickOn(`${oldCatgoryName}:see-actions`);
|
|
await puppeteerUtils(puppeteerPage).clickOn(`${oldCatgoryName}:edit-category-mode`);
|
|
await puppeteerUtils(puppeteerPage).clearInputAndTypeInto(`${oldCatgoryName}:edit-category-name-text`, newCategoryName);
|
|
await puppeteerUtils(puppeteerPage).clickOn(`${oldCatgoryName}:submit-category-edit`);
|
|
},
|
|
|
|
async deleteCategory(categoryName) {
|
|
await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:see-actions`);
|
|
await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:delete-category`);
|
|
},
|
|
|
|
async createLabel(categoryName, labelName) {
|
|
await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:see-actions`);
|
|
await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:add-new-label-to-category`);
|
|
await puppeteerUtils(puppeteerPage).typeInto(`${categoryName}:new-label-name`, labelName);
|
|
await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:submit-label`);
|
|
},
|
|
|
|
async deleteLabel(categoryName, labelName) {
|
|
await this.expandCategory(categoryName);
|
|
await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:${labelName}:see-actions`);
|
|
await puppeteerUtils(puppeteerPage).clickOn( `${categoryName}:${labelName}:delete-label`);
|
|
},
|
|
|
|
async renameLabel(categoryName, oldLabelName, newLabelName) {
|
|
await this.expandCategory(categoryName);
|
|
await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:${oldLabelName}:see-actions`);
|
|
await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:${oldLabelName}:edit-label`);
|
|
await puppeteerUtils(puppeteerPage).clearInputAndTypeInto(
|
|
`${categoryName}:${oldLabelName}:edit-label-name`,
|
|
newLabelName
|
|
);
|
|
await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:${oldLabelName}:submit-label-edit`);
|
|
}
|
|
});
|
|
|
|
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];
|
|
}
|