Files
cellxgene/client/__tests__/e2e/puppeteerUtils.js
T
Matt Weiden 8a62c9f027 Add undo/redo smoke tests for annotations (#1175)
* 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
2020-02-26 12:10:03 -08:00

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];
}