Files
metabase/e2e/support/helpers/e2e-downloads-helpers.ts
Chris Vandoodewaard 519834f9ee Consolidate sharing e2e tests (#83657)
* Remove duplicate public sharing e2e tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Merge sharing reproductions e2e tests that share setup

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Merge downloads e2e tests that share setup

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Merge public sharing e2e tests that share setup

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Repair downloads e2e tests that cannot fail

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Repair sharing reproductions e2e tests that cannot fail

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Drop unused setup in sharing reproductions e2e tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Remove duplicate subscriptions e2e tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Repair public sharing e2e tests that cannot fail

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Drop unused setup in downloads e2e tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Drop unused setup in public sharing e2e tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Remove duplicate alert e2e tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Merge alert e2e tests that share setup

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Repair alert e2e tests that cannot fail

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Drop unused setup in alert e2e tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Remove duplicate public sharing e2e tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Merge subscriptions e2e tests that share setup

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Repair subscriptions e2e tests that cannot fail

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Drop unused setup in subscriptions e2e tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Fold public dashboard link creation e2e test into 20393 repro

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fold 30314 subscription cancel e2e test into 14117 test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Fold 16108 tooltip e2e test into download format test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Merge public sharing e2e tests that share setup

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Place merged 18352 card below the Orders card

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Type a name before Escape in the 24629 subscriptions e2e test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Repair public sharing e2e tests that cannot fail

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Tag sharing e2e tests that need maildev as external

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Drop unused setup in public sharing e2e tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Fix popover check after Escape in dashboard downloads e2e test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Keep the webhook channel in the 12349 alert e2e test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Tighten subscriptions e2e checks found in parity review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Tighten sharing reproductions e2e checks found in parity review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Restore admin and UI-created link coverage in public sharing e2e tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Jc2P2sKm39MghfKPxCSiv

* Fix 7 sharing e2e failures from CI

* Restore 48402 coverage and merge more sharing e2e tests

- Restore the metabase#48402 delete-alert step in the 12349 test and check the delete modal
- Make 18669 check the email filter value and 21559 check the card image
- Merge public dashboard download flag tests with the real downloads
- Merge static-viz, branding, and default-filter subscription tests
- Merge dashboard PDF and PNG download tests and check download parameters
- Merge the two background=false public dashboard tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Address review on sharing e2e consolidation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix sharing e2e CI failures

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:39:59 -04:00

252 lines
7.6 KiB
TypeScript

import {
getDashboardCard,
getDashboardCardMenu,
getEmbeddedDashboardCardMenu,
} from "./e2e-dashboard-helpers";
import { popover } from "./e2e-ui-elements-helpers";
interface DownloadAndAssertParams {
/** The type of the file we're downloading */
fileType: "csv" | "xlsx";
/** needed only for saved questions */
questionId?: number;
dashcardId?: number;
/** a download URL when you want to override the one generated by this util */
downloadUrl?: string;
downloadMethod?: "POST" | "GET";
/** downloading is tested on an embedded resource */
isEmbed?: boolean;
/** downloading is tested on a dashboard */
isDashboard: boolean;
publicUuid?: string;
dashboardId?: number;
enableFormatting?: boolean;
pivoting?: "pivoted" | "non-pivoted";
assertStatusCode?: number;
/** Assert that parameters in request body match expected values */
assertParameters?: any[];
/** Assert on the downloaded file content (POST downloads only) */
assertResponseBody?: (body: string) => void;
waitForDismiss?: boolean;
}
export interface DownloadRequestData {
request: {
url: string;
method: string;
body?: any;
headers?: Record<string, string>;
};
response: {
statusCode: number;
headers: Record<string, string>;
contentType: string | null;
};
}
export const exportFromDashcard = (format: string) => {
cy.get("[aria-label='Download results']").click();
popover().within(() => {
cy.findByText(format).click();
cy.findByTestId("download-results-button").click();
});
cy.findByTestId("status-root-container")
.invoke("text")
.should("match", /Downloading|Done/);
};
/**
* Trigger the download of CSV or XLSX files and assert on the request/response.
* It applies to both unsaved questions (queries) and the saved ones.
* This function verifies that the correct HTTP request is made with proper parameters
* and that the response has the correct content type.
*/
export function downloadAndAssert({
fileType,
questionId,
publicUuid,
dashcardId,
dashboardId,
downloadUrl,
downloadMethod = "POST",
isDashboard,
isEmbed = false,
enableFormatting = true,
pivoting,
assertStatusCode,
assertParameters,
assertResponseBody,
waitForDismiss = true,
}: DownloadAndAssertParams) {
const { method, endpoint } = downloadUrl
? { method: downloadMethod, endpoint: downloadUrl }
: getEndpoint({
fileType,
questionId,
publicUuid,
dashcardId,
dashboardId,
});
/**
* Please see the official Cypress example for more details:
* https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/testing-dom__download/cypress/integration/form-submission-spec.js
*/
if (method === "POST") {
// For POST requests, we need to redirect to prevent Cypress from hanging
cy.intercept(method, endpoint, (req) => {
/**
* We must redirect in order to avoid Cypress being stuck on waiting for the new page to load.
* Intentionally redirecting to a non-existing page.
*
* Explanation:
* If we redirect to ANY of the existing pages, there's a lot of requests that need to complete for that page.
* - This helper function is usually the last piece of code to execute in any given test.
* - As soon as the assertions are complete, the new test starts
* - Assertions are usually faster than all of the previously mentioned requests from the redirect
* - This results in the next test being polluted with the requests that didn't finish from the last one.
* - Those "spill-over" requests end up in the beforeEach hook of the next test and can have unexpected results.
*/
req.continue((res) => {
const expectedContentType =
fileType === "csv"
? "text/csv"
: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
expect(res.statusCode).to.eq(200);
expect(res.headers["content-type"]).to.include(expectedContentType);
if (assertParameters) {
const parameters = JSON.parse(
new URLSearchParams(req.body).get("parameters") ?? "null",
);
expect(parameters).to.have.lengthOf(assertParameters.length);
assertParameters.forEach((expectedParam, index) => {
expect(parameters[index]).to.deep.include(expectedParam);
});
}
assertResponseBody?.(res.body);
// After assertions pass, redirect to prevent navigation
res.send({ statusCode: 302, headers: { location: "/foo" } });
});
}).as("fileDownload");
} else {
// For GET requests the server redirects automatically so no need to do it manually
cy.intercept(method, endpoint).as("fileDownload");
}
cy.log(`Downloading ${fileType} file`);
if (isDashboard) {
if (isEmbed) {
getDashboardCard().realHover();
getEmbeddedDashboardCardMenu().click();
} else {
getDashboardCardMenu().click();
}
}
cy.findByLabelText("Download results").should("be.visible").click();
popover().within(() => {
cy.findByText(`.${fileType}`).click();
cy.findByTestId("keep-data-formatted")
.as("keep-data-formatted")
.then(($checkbox) => {
const isChecked = $checkbox.prop("checked");
if (enableFormatting !== isChecked) {
cy.get("@keep-data-formatted").click();
}
});
if (pivoting != null) {
cy.findByTestId("keep-data-pivoted")
.as("keep-data-pivoted")
.then(($checkbox) => {
const isChecked = $checkbox.prop("checked");
const shouldPivot = pivoting === "pivoted";
if (shouldPivot !== isChecked) {
cy.get("@keep-data-pivoted").click();
}
});
}
cy.findByTestId("download-results-button").click();
});
cy.wait("@fileDownload").then(({ response }) => {
if (assertStatusCode) {
expect(response?.statusCode).to.eq(assertStatusCode);
}
if (waitForDismiss) {
ensureDownloadStatusDismissed();
}
});
}
export function assertNotEmptyObject(data: any) {
return Object.keys(data).length > 0;
}
type GetEndPointParams = Pick<
DownloadAndAssertParams,
"fileType" | "questionId" | "publicUuid" | "dashcardId" | "dashboardId"
>;
function getEndpoint({
fileType,
questionId,
publicUuid,
dashcardId,
dashboardId,
}: GetEndPointParams): {
endpoint: string;
method: "POST" | "GET";
} {
if (dashcardId != null && dashboardId != null) {
return {
endpoint: `api/dashboard/${dashboardId}/dashcard/${dashcardId}/card/${questionId}/query/${fileType}**`,
method: "POST",
};
}
if (publicUuid) {
if (dashcardId) {
return {
endpoint: `api/public/dashboard/${publicUuid}/dashcard/${dashcardId}/card/*/${fileType}**`,
method: "POST",
};
}
return {
endpoint: `/public/question/${publicUuid}.${fileType}**`,
method: "GET",
};
}
const questionEndpoint = `/api/card/${questionId}/query/${fileType}**`;
const queryEndpoint = `/api/dataset/${fileType}**`;
return {
endpoint: questionId ? questionEndpoint : queryEndpoint,
method: "POST",
};
}
export function ensureDownloadStatusDismissed() {
// Upon successful export, we display a status popup that automatically closes after a set time.
// However, Cypress sometimes hangs after file downloads, making it difficult to determine if
// the status popup has already closed on its own or if we need to close it manually which makes
// any attempts to close it flaky. As a workaround we wait until it gets removed by itself.
cy.findByTestId("status-root-container")
.contains("Download", { timeout: 10000 })
.should("not.exist");
}