* 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>
252 lines
7.6 KiB
TypeScript
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");
|
|
}
|