From b1c141c078ad8a0599b511d7e74989bcb26caea7 Mon Sep 17 00:00:00 2001 From: Timmy Huang Date: Mon, 3 Jun 2024 13:14:35 -0700 Subject: [PATCH] test: Add deep zoom e2e tests (#959) --- client/__tests__/e2e/cellxgeneActions.ts | 98 +++++- client/__tests__/e2e/data.ts | 42 ++- client/__tests__/e2e/e2e.test.ts | 387 +++++++++++++++++----- client/playwright.config.ts | 8 +- client/src/components/embedding/index.tsx | 14 +- client/src/components/graph/graph.tsx | 8 +- client/src/components/menubar/index.tsx | 3 +- client/src/components/util/truncate.tsx | 11 +- client/src/util/constants.ts | 4 + 9 files changed, 474 insertions(+), 101 deletions(-) diff --git a/client/__tests__/e2e/cellxgeneActions.ts b/client/__tests__/e2e/cellxgeneActions.ts index 795e63120..89db19a50 100644 --- a/client/__tests__/e2e/cellxgeneActions.ts +++ b/client/__tests__/e2e/cellxgeneActions.ts @@ -3,19 +3,33 @@ import { Page, TestInfo, expect } from "@playwright/test"; import { Classes } from "@blueprintjs/core"; import { takeSnapshot } from "@chromatic-com/playwright"; import { clearInputAndTypeInto, tryUntil, typeInto } from "./puppeteerUtils"; +import { + GRAPH_AS_IMAGE_TEST_ID, + LAYOUT_CHOICE_TEST_ID, +} from "../../src/util/constants"; interface Coordinate { x: number; y: number; } -export async function drag( - testId: string, - start: Coordinate, - end: Coordinate, - page: Page, - lasso = false -): Promise { +const WAIT_FOR_SWITCH_LAYOUT_MS = 2_000; + +export async function drag({ + testId, + testInfo, + start, + end, + page, + lasso = false, +}: { + testId: string; + testInfo: TestInfo; + start: Coordinate; + end: Coordinate; + page: Page; + lasso?: boolean; +}): Promise { const layout = await page.getByTestId(testId); const box = await layout.boundingBox(); if (!box) throw new Error("bounding box not found"); @@ -37,6 +51,8 @@ export async function drag( } await page.mouse.up(); + + await snapshotTestGraph(page, testInfo); } export async function scroll({ @@ -710,7 +726,8 @@ export async function addGeneToSearch( export async function subset( coordinatesAsPercent: CoordinateAsPercent, - page: Page + page: Page, + testInfo: TestInfo ): Promise { // In order to deselect the selection after the subset, make sure we have some clear part // of the scatterplot we can click on @@ -722,13 +739,14 @@ export async function subset( coordinatesAsPercent, page ); - await drag( - "layout-graph", - lassoSelection.start, - lassoSelection.end, + await drag({ + testId: "layout-graph", + testInfo, + start: lassoSelection.start, + end: lassoSelection.end, page, - true - ); + lasso: true, + }); await page.getByTestId("subset-button").click(); const clearCoordinate = await calcCoordinate("layout-graph", 0.5, 0.99, page); await clickOnCoordinate("layout-graph", clearCoordinate, page); @@ -769,19 +787,67 @@ export async function assertUndoRedo( } export async function snapshotTestGraph(page: Page, testInfo: TestInfo) { - const buttonID = "capture-and-display-graph"; const imageID = "graph-image"; + await waitUntilNoSkeletonDetected(page); + await tryUntil( async () => { - await page.getByTestId(buttonID).click({ force: true }); + await page.getByTestId(GRAPH_AS_IMAGE_TEST_ID).click({ force: true }); await page.getByTestId(imageID).waitFor(); await takeSnapshot(page, testInfo); + + /** + * (thuang): Remove the image in the DOM after taking the snapshot + */ + await page.getByTestId(GRAPH_AS_IMAGE_TEST_ID).click({ force: true }); }, { page } ); } +export async function selectLayout(layoutChoice: string, page: Page) { + await page.getByTestId(LAYOUT_CHOICE_TEST_ID).click(); + + /** + * (thuang): For blueprint radio buttons, we need to tab first to go to the + * currently selected option before we can use the arrow keys to navigate + */ + await page.keyboard.press("Tab"); + + const layoutChoices = await await page + .getByTestId(RegExp(`^${LAYOUT_CHOICE_TEST_ID}-label-`)) + .allInnerTexts(); + + const currentlySelectedLayout = await page + .locator("label") + .filter({ has: page.getByRole("radio", { checked: true }) }) + .innerText(); + + const currentIndex = layoutChoices.indexOf(currentlySelectedLayout); + const targetIndex = layoutChoices.findIndex((choice) => + choice.includes(layoutChoice) + ); + + const relativePosition = targetIndex - currentIndex; + + if (relativePosition > 0) { + // press down arrow N times + for (let i = 0; i < relativePosition; i += 1) { + await page.keyboard.press("ArrowDown"); + } + } else { + // press up arrow N times + for (let i = 0; i < Math.abs(relativePosition); i += 1) { + await page.keyboard.press("ArrowUp"); + } + } + + await page.getByTestId(LAYOUT_CHOICE_TEST_ID).click(); + + await page.waitForTimeout(WAIT_FOR_SWITCH_LAYOUT_MS); +} + /* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */ diff --git a/client/__tests__/e2e/data.ts b/client/__tests__/e2e/data.ts index 911098662..e5e90d0ee 100644 --- a/client/__tests__/e2e/data.ts +++ b/client/__tests__/e2e/data.ts @@ -44,6 +44,15 @@ export const datasets = { count: "930", }, ], + invalidLasso: [ + { + /** + * (thuang): This creates a straight line along x, which is an invalid lasso. + */ + "coordinates-as-percent": { x1: 0.1, y1: 0.25, x2: 0.7, y2: 0.25 }, + count: "2638", + }, + ], categorical: [ { metadata: "louvain", @@ -59,7 +68,6 @@ export const datasets = { }, ], }, - diffexp: { category: "louvain", cellset1: { @@ -70,7 +78,6 @@ export const datasets = { }, pop2Gene: "NKG7", }, - genes: { bulkadd: ["S100A8", "FCGR3A", "LGALS2", "GSTP1"], search: "ACD", @@ -145,6 +152,19 @@ export const datasets = { default: "109", withSubset: "94", }, + embeddingChoice: { + original: "umap", + somethingElse: "pca", + }, + /** + * (thuang): Unused, since homeButton is only available on spatial datasets. + * However, it's included here to satisfy the type checker. + */ + homeButton: { + pan: { + "coordinates-as-percent": { x1: 0, y1: 0, x2: 0, y2: 0 }, + }, + }, }, "super-cool-spatial.cxg": { title: "super-cool-spatial.cxg", @@ -193,6 +213,15 @@ export const datasets = { count: "2025", }, ], + invalidLasso: [ + { + /** + * (thuang): This creates a straight line along x, which is an invalid lasso. + */ + "coordinates-as-percent": { x1: 0.1, y1: 0.25, x2: 0.7, y2: 0.25 }, + count: "2881", + }, + ], categorical: [ { metadata: "cell_type", @@ -294,5 +323,14 @@ export const datasets = { default: "52", withSubset: "47", }, + embeddingChoice: { + original: "spatial", + somethingElse: "prop", + }, + homeButton: { + pan: { + "coordinates-as-percent": { x1: 0.75, y1: 0.75, x2: 0.2, y2: 0.2 }, + }, + }, }, }; diff --git a/client/__tests__/e2e/e2e.test.ts b/client/__tests__/e2e/e2e.test.ts index 5f70a0cf7..308c1e1a8 100644 --- a/client/__tests__/e2e/e2e.test.ts +++ b/client/__tests__/e2e/e2e.test.ts @@ -8,7 +8,7 @@ /* eslint-disable compat/compat -- not ran in the browser */ /* eslint-disable no-await-in-loop -- await in loop is needed to emulate sequential user actions */ -import { Page, TestInfo } from "@playwright/test"; +import { Download, Page, TestInfo } from "@playwright/test"; import { test, expect, takeSnapshot } from "@chromatic-com/playwright"; import os from "os"; import fs from "fs/promises"; @@ -51,6 +51,7 @@ import { assertUndoRedo, snapshotTestGraph, getAllCategories, + selectLayout, } from "./cellxgeneActions"; import { datasets } from "./data"; @@ -64,7 +65,7 @@ import { import { goToPage } from "../util/helpers"; import { SCALE_MAX } from "../../src/util/constants"; -const { describe } = test; +const { describe, skip } = test; // geneset CRUD const genesetToDeleteName = "geneset_to_delete"; @@ -92,17 +93,19 @@ const geneToRequestInfo = "SIK1"; const genesetDescriptionString = "fourth_gene_set: fourth description"; const genesetToCheckForDescription = "fourth_gene_set"; +const WAIT_FOR_AFTER_RESIZE_MS = 2_000; +const CHECK_HOME_BUTTON_MS = 500; + // TODO #754 test.beforeEach(mockSetup); -const testDatasets = [ - DATASET, - "super-cool-spatial.cxg", -] as (keyof typeof datasets)[]; +const SPATIAL_DATASET = "super-cool-spatial.cxg"; + +const testDatasets = [DATASET, SPATIAL_DATASET] as (keyof typeof datasets)[]; const testURLs = { [DATASET]: testURL, - "super-cool-spatial.cxg": pageURLSpatial, + [SPATIAL_DATASET]: pageURLSpatial, }; for (const testDataset of testDatasets) { @@ -190,6 +193,18 @@ for (const testDataset of testDatasets) { }); }); + test("resize graph", async ({ page }, testInfo) => { + await goToPage(page, url); + + await snapshotTestGraph(page, testInfo); + + await page.setViewportSize({ width: 600, height: 600 }); + + await page.waitForTimeout(WAIT_FOR_AFTER_RESIZE_MS); + + await snapshotTestGraph(page, testInfo); + }); + describe("cell selection", () => { test("selects all cells cellset 1", async ({ page }, testInfo) => { await goToPage(page, url); @@ -208,20 +223,74 @@ for (const testDataset of testDatasets) { await snapshotTestGraph(page, testInfo); }); - test("selects cells via lasso", async ({ page }, testInfo) => { + test("bug fix: invalid lasso cancels lasso overlay", async ({ + page, + }, testInfo) => { await goToPage(page, url); - for (const cellset of data.cellsets.lasso) { + + for (const cellset of data.cellsets.invalidLasso) { const cellset1 = await calcDragCoordinates( "layout-graph", cellset["coordinates-as-percent"], page ); - await drag("layout-graph", cellset1.start, cellset1.end, page, true); + await drag({ + page, + testInfo, + testId: "layout-graph", + start: cellset1.start, + end: cellset1.end, + lasso: true, + }); const cellCount = await getCellSetCount(1, page); expect(cellCount).toBe(cellset.count); + + await snapshotTestGraph(page, testInfo); + } + }); + + test("selects cells via lasso", async ({ page }, testInfo) => { + await goToPage(page, url); + + const originalCellCount = await getCellSetCount(1, page); + + for (const cellset of data.cellsets.lasso) { + const cellset1 = await calcDragCoordinates( + "layout-graph", + cellset["coordinates-as-percent"], + page + ); + + await drag({ + page, + testInfo, + testId: "layout-graph", + start: cellset1.start, + end: cellset1.end, + lasso: true, + }); + + expect(await getCellSetCount(1, page)).toBe(cellset.count); + + await snapshotTestGraph(page, testInfo); + + // switch layout should retain the selection + const { original, somethingElse } = data.embeddingChoice; + + await selectLayout(somethingElse, page); + + expect(await getCellSetCount(1, page)).toBe(cellset.count); + + await snapshotTestGraph(page, testInfo); + + // switch back to original layout should reset the selection + await selectLayout(original, page); + + expect(await getCellSetCount(1, page)).toBe(originalCellCount); + await snapshotTestGraph(page, testInfo); } }); @@ -262,7 +331,13 @@ for (const testDataset of testDatasets) { page ); - await drag(histBrushableAreaId, coords.start, coords.end, page); + await drag({ + page, + testInfo, + testId: histBrushableAreaId, + start: coords.start, + end: coords.end, + }); const cellCount = await getCellSetCount(1, page); @@ -284,16 +359,37 @@ for (const testDataset of testDatasets) { await page.getByTestId("subset-button").click(); - for (const label of Object.keys( - data.subset.categorical - ) as (keyof typeof data.subset.categorical)[]) { - const categories = await getAllCategoriesAndCounts(label, page); + await assertCategoricalCounts(); - expect(categories).toMatchObject(data.subset.categorical[label]); + // switch layout should retain the subset + const { original, somethingElse } = data.embeddingChoice; - await snapshotTestGraph(page, testInfo); + await selectLayout(somethingElse, page); + + await assertCategoricalCounts(); + + await snapshotTestGraph(page, testInfo); + + // switch back to original layout should retain the subset too + await selectLayout(original, page); + + await assertCategoricalCounts(); + + await snapshotTestGraph(page, testInfo); + + async function assertCategoricalCounts() { + for (const label of Object.keys( + data.subset.categorical + ) as (keyof typeof data.subset.categorical)[]) { + const categories = await getAllCategoriesAndCounts(label, page); + + expect(categories).toMatchObject(data.subset.categorical[label]); + + await snapshotTestGraph(page, testInfo); + } } }); + test("subset - categories with zero cells are filtered out", async ({ page, }) => { @@ -327,13 +423,14 @@ for (const testDataset of testDatasets) { page ); - await drag( - "layout-graph", - lassoSelection.start, - lassoSelection.end, + await drag({ page, - true - ); + testInfo, + testId: "layout-graph", + start: lassoSelection.start, + end: lassoSelection.end, + lasso: true, + }); const cellCount = await getCellSetCount(1, page); @@ -352,7 +449,15 @@ for (const testDataset of testDatasets) { data.clip["coordinates-as-percent"], page ); - await drag(histBrushableAreaId, coords.start, coords.end, page); + + await drag({ + page, + testInfo, + testId: histBrushableAreaId, + start: coords.start, + end: coords.end, + }); + const cellCount = await getCellSetCount(1, page); expect(cellCount).toBe(data.clip.count); @@ -401,10 +506,88 @@ for (const testDataset of testDatasets) { page ); - await drag("layout-graph", panCoords.start, panCoords.end, page); + await drag({ + page, + testInfo, + testId: "layout-graph", + start: panCoords.start, + end: panCoords.end, + }); await page.evaluate("window.scrollBy(0, 1000);"); + + await snapshotTestGraph(page, testInfo); + + // switch layout and back should reset pan and zoom + const { original, somethingElse } = data.embeddingChoice; + + await selectLayout(somethingElse, page); + await snapshotTestGraph(page, testInfo); + + await selectLayout(original, page); + + await snapshotTestGraph(page, testInfo); + }); + + test("home button", async ({ page }, testInfo) => { + skip(testDataset !== SPATIAL_DATASET, "Only run on spatial dataset"); + + await goToPage(page, url); + + const homeButton = page.getByText("Re-center Embedding"); + + await expect(homeButton).not.toBeVisible(); + + const { + homeButton: { pan }, + } = data; + + const { start, end } = await calcDragCoordinates( + "layout-graph", + pan["coordinates-as-percent"], + page + ); + + await page.getByTestId("mode-pan-zoom").click(); + + await tryUntil( + async () => { + await drag({ + page, + testInfo, + testId: "layout-graph", + start, + end, + }); + + /** + * (thuang): We need to do the same drag twice, since the first drag + * doesn't move the image completely out of the viewport + */ + await drag({ + page, + testInfo, + testId: "layout-graph", + start, + end, + }); + + await expect(homeButton).toBeVisible({ + timeout: CHECK_HOME_BUTTON_MS, + }); + }, + { page } + ); + + await homeButton.click(); + + await tryUntil( + async () => { + await expect(homeButton).not.toBeVisible(); + }, + { page } + ); }); }); @@ -463,7 +646,13 @@ for (const testDataset of testDatasets) { await tryUntil( async () => { - await drag("layout-graph", panCoords.start, panCoords.end, page); + await drag({ + page, + testInfo, + testId: "layout-graph", + start: panCoords.start, + end: panCoords.end, + }); const terminalCoordinates = await getElementCoordinates( `centroid-label`, @@ -535,13 +724,15 @@ for (const testDataset of testDatasets) { page ); - await drag( - "layout-graph", - lassoSelection.start, - lassoSelection.end, + await drag({ page, - true - ); + testInfo, + testId: "layout-graph", + start: lassoSelection.start, + end: lassoSelection.end, + lasso: true, + }); + await expect(page.getByTestId("lasso-element")).toBeVisible(); const initialCount = await getCellSetCount(1, page); @@ -577,13 +768,14 @@ for (const testDataset of testDatasets) { await snapshotTestGraph(page, testInfo); - await drag( - "layout-graph", - lassoSelection.start, - lassoSelection.end, + await drag({ page, - true - ); + testInfo, + testId: "layout-graph", + start: lassoSelection.start, + end: lassoSelection.end, + lasso: true, + }); await snapshotTestGraph(page, testInfo); @@ -603,7 +795,14 @@ for (const testDataset of testDatasets) { page ); - await drag("layout-graph", panCoords.start, panCoords.end, page); + await drag({ + page, + testInfo, + testId: "layout-graph", + start: panCoords.start, + end: panCoords.end, + lasso: true, + }); await snapshotTestGraph(page, testInfo); @@ -631,7 +830,7 @@ for (const testDataset of testDatasets) { for (const option of options) { describe(`geneSET crud operations and interactions ${option.tag}`, () => { test("brush on geneset mean", async ({ page }, testInfo) => { - await setup({ option, page, url }); + await setup({ option, page, url, testInfo }); await createGeneset(meanExpressionBrushGenesetName, page); await addGeneToSetAndExpand( meanExpressionBrushGenesetName, @@ -652,7 +851,14 @@ for (const testDataset of testDatasets) { page ); - await drag(histBrushableAreaId, coords.start, coords.end, page); + await drag({ + page, + testInfo, + testId: histBrushableAreaId, + start: coords.start, + end: coords.end, + }); + await page.getByTestId(`cellset-button-1`).click(); const cellCount = await getCellSetCount(1, page); @@ -667,7 +873,7 @@ for (const testDataset of testDatasets) { }); test("color by mean expression", async ({ page }, testInfo) => { - await setup({ option, page, url }); + await setup({ option, page, url, testInfo }); await createGeneset(meanExpressionBrushGenesetName, page); await addGeneToSetAndExpand( meanExpressionBrushGenesetName, @@ -681,8 +887,8 @@ for (const testDataset of testDatasets) { }); test("color by mean expression changes sorting of categories in 'cell_type'", async ({ page, - }) => { - await setup({ option, page, url }); + }, testInfo) => { + await setup({ option, page, url, testInfo }); const categories = await page .locator('[data-testid*=":category-expand"]') @@ -738,7 +944,7 @@ for (const testDataset of testDatasets) { // this test will take longer if we're running against a deployment if (runningAgainstDeployment) test.slow(); - await setup({ option, page, url }); + await setup({ option, page, url, testInfo }); const { category, cellset1, cellset2 } = data.diffexp; @@ -800,7 +1006,9 @@ for (const testDataset of testDatasets) { await snapshotTestGraph(page, testInfo); }); - test("create a new geneset and undo/redo", async ({ page }) => { + test("create a new geneset and undo/redo", async ({ + page, + }, testInfo) => { /** * (thuang): Test is flaky, so we need to retry until it passes */ @@ -808,7 +1016,7 @@ for (const testDataset of testDatasets) { async () => { if (option.withSubset) return; - await setup({ option, page, url }); + await setup({ option, page, url, testInfo }); await waitUntilNoSkeletonDetected(page); @@ -830,13 +1038,13 @@ for (const testDataset of testDatasets) { ); }); - test("edit geneset name and undo/redo", async ({ page }) => { + test("edit geneset name and undo/redo", async ({ page }, testInfo) => { /** * (thuang): Test is flaky, so we need to retry until it passes */ await tryUntil( async () => { - await setup({ option, page, url }); + await setup({ option, page, url, testInfo }); await createGeneset(editableGenesetName, page); await editGenesetName(editableGenesetName, newGenesetName, page); @@ -852,7 +1060,7 @@ for (const testDataset of testDatasets) { ); }); - test("delete a geneset and undo/redo", async ({ page }) => { + test("delete a geneset and undo/redo", async ({ page }, testInfo) => { /** * (thuang): Test is flaky, so we need to retry until it passes */ @@ -860,7 +1068,7 @@ for (const testDataset of testDatasets) { async () => { if (option.withSubset) return; - await setup({ option, page, url }); + await setup({ option, page, url, testInfo }); await createGeneset(genesetToDeleteName, page); @@ -876,10 +1084,10 @@ for (const testDataset of testDatasets) { ); }); - test("geneset description", async ({ page }) => { + test("geneset description", async ({ page }, testInfo) => { if (option.withSubset) return; - await setup({ option, page, url }); + await setup({ option, page, url, testInfo }); await createGeneset( genesetToCheckForDescription, @@ -896,13 +1104,15 @@ for (const testDataset of testDatasets) { }); describe(`GENE crud operations and interactions ${option.tag}`, () => { - test("add a gene to geneset and undo/redo", async ({ page }) => { + test("add a gene to geneset and undo/redo", async ({ + page, + }, testInfo) => { /** * (thuang): Test is flaky, so we need to retry until it passes */ await tryUntil( async () => { - await setup({ option, page, url }); + await setup({ option, page, url, testInfo }); await createGeneset(setToAddGeneTo, page); await addGeneToSetAndExpand(setToAddGeneTo, geneToAddToSet, page); await assertGeneExistsInGeneset(geneToAddToSet, page); @@ -918,7 +1128,7 @@ for (const testDataset of testDatasets) { }); test("expand gene and brush", async ({ page }, testInfo) => { - await setup({ option, page, url }); + await setup({ option, page, url, testInfo }); await createGeneset(brushThisGeneGeneset, page); await addGeneToSetAndExpand( brushThisGeneGeneset, @@ -941,7 +1151,13 @@ for (const testDataset of testDatasets) { await snapshotTestGraph(page, testInfo); - await drag(histBrushableAreaId, coords.start, coords.end, page); + await drag({ + page, + testInfo, + testId: histBrushableAreaId, + start: coords.start, + end: coords.end, + }); await snapshotTestGraph(page, testInfo); @@ -957,7 +1173,7 @@ for (const testDataset of testDatasets) { }); test("color by gene in geneset", async ({ page }, testInfo) => { - await setup({ option, page, url }); + await setup({ option, page, url, testInfo }); await createGeneset(meanExpressionBrushGenesetName, page); @@ -971,7 +1187,9 @@ for (const testDataset of testDatasets) { await assertColorLegendLabel("SIK1", page); await snapshotTestGraph(page, testInfo); }); - test("delete gene from geneset and undo/redo", async ({ page }) => { + test("delete gene from geneset and undo/redo", async ({ + page, + }, testInfo) => { /** * (thuang): Test is flaky, so we need to retry until it passes */ @@ -979,7 +1197,7 @@ for (const testDataset of testDatasets) { async () => { if (option.withSubset) return; - await setup({ option, page, url }); + await setup({ option, page, url, testInfo }); await createGeneset(setToRemoveFrom, page); await addGeneToSetAndExpand(setToRemoveFrom, geneToRemove, page); @@ -1000,7 +1218,7 @@ for (const testDataset of testDatasets) { test("open gene info card and hide/remove", async ({ page, }, testInfo) => { - await setup({ option, page, url }); + await setup({ option, page, url, testInfo }); await addGeneToSearch(geneToRequestInfo, page); await snapshotTestGraph(page, testInfo); @@ -1039,7 +1257,7 @@ for (const testDataset of testDatasets) { } describe(`Image Download`, () => { - async function navigateToAndSnapshotImage( + async function snapshotDownloadedImage( page: Page, info: TestInfo, path: string @@ -1059,6 +1277,7 @@ for (const testDataset of testDatasets) { img.style.top = "0"; document.body.appendChild(img); }, imageFile); + await takeSnapshot(page, info); // remove the image from the dom @@ -1067,33 +1286,46 @@ for (const testDataset of testDatasets) { img?.parentNode?.removeChild(img); }); } + async function downloadAndSnapshotImage(page: Page, info: TestInfo) { - const graphPromise = page.waitForEvent("download"); + const downloads: Download[] = []; + + page.on("download", (downloadData) => { + downloads.push(downloadData); + }); + await page.getByTestId("download-graph-button").click(); - const graphDownload = await graphPromise; + + await tryUntil( + async () => { + expect(downloads.length).toBe( + /** + * (thuang): If it's a categorical test, we expect 2 downloads + */ + info.title.includes("categorical") ? 2 : 1 + ); + }, + { page } + ); + + const [graphDownload, legendDownload] = downloads; const tmp = os.tmpdir(); + // make the title safe for file system const safeTitle = info.title.replace(/[^a-z0-9]/gi, "_"); const graphPath = `${tmp}/${safeTitle}/${graphDownload.suggestedFilename()}`; - let legendPromise; - if (info.title.includes("categorical")) { - // this will capture a separate legend download if the colorby is categorical - legendPromise = page.waitForEvent("download"); - } await graphDownload.saveAs(graphPath); - if (legendPromise) { - // awaiting the legend download needs to occur after the graph image has been downloaded - // otherwise the legend download will not be triggered and the await will hang - const legendDownload = await legendPromise; + if (legendDownload) { const legendPath = `${tmp}/${safeTitle}/${legendDownload.suggestedFilename()}`; await legendDownload.saveAs(legendPath); - await navigateToAndSnapshotImage(page, info, legendPath); + await snapshotDownloadedImage(page, info, legendPath); } - await navigateToAndSnapshotImage(page, info, graphPath); + + await snapshotDownloadedImage(page, info, graphPath); } test("with continuous legend", async ({ page }, testInfo) => { @@ -1104,6 +1336,7 @@ for (const testDataset of testDatasets) { await downloadAndSnapshotImage(page, testInfo); }); + test("with categorical legend", async ({ page }, testInfo) => { await goToPage(page, url); @@ -1143,14 +1376,16 @@ async function setup({ option: { withSubset }, page, url, + testInfo, }: { option: { withSubset: boolean; tag: string }; page: Page; url: string; + testInfo: TestInfo; }) { await goToPage(page, url); if (withSubset) { - await subset({ x1: 0.1, y1: 0.15, x2: 0.8, y2: 0.85 }, page); + await subset({ x1: 0.1, y1: 0.15, x2: 0.8, y2: 0.85 }, page, testInfo); } } diff --git a/client/playwright.config.ts b/client/playwright.config.ts index 7e07ee0ea..a6207d612 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -1,4 +1,5 @@ import { ReporterDescription, defineConfig, devices } from "@playwright/test"; +import { ChromaticConfig } from "@chromatic-com/playwright"; import { testURL } from "./__tests__/common/constants"; /** @@ -48,7 +49,7 @@ const PLAYWRIGHT_REPORTER = process.env.CI /** * See https://playwright.dev/docs/test-configuration. */ -export default defineConfig({ +export default defineConfig({ expect: { /** * Maximum time expect() should wait for the condition to be met. @@ -69,6 +70,11 @@ export default defineConfig({ reporter: PLAYWRIGHT_REPORTER, /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { + /** + * (thuang): We need to explicitly capture canvas with `snapshotTestGraph` + * so the auto snapshot feature is not useful + */ + disableAutoSnapshot: true, acceptDownloads: true, /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ actionTimeout: 0, diff --git a/client/src/components/embedding/index.tsx b/client/src/components/embedding/index.tsx index 781a5f0df..c3c5f8967 100644 --- a/client/src/components/embedding/index.tsx +++ b/client/src/components/embedding/index.tsx @@ -17,6 +17,7 @@ import { getDiscreteCellEmbeddingRowIndex } from "../../util/stateManager/viewSt import { track } from "../../analytics"; import { EVENTS } from "../../analytics/events"; import { RootState } from "../../reducers"; +import { LAYOUT_CHOICE_TEST_ID } from "../../util/constants"; type Props = RootState; @@ -72,7 +73,7 @@ class Embedding extends React.PureComponent { >