Files
HiveGrid-Wireframes/hivegrid-cloud/tests/e2e/subscription-smoke.spec.ts
T

975 lines
51 KiB
TypeScript

import { expect, test, type Locator, type Page } from "./utils/test";
import { getTenantCredentials, loginAsTenantUser, tenantSettingsAccountPath } from "./utils/auth";
import { expectNoCriticalA11yViolations } from "./utils/axe";
import { compactMobileViewport, expectNoHorizontalOverflow, narrowMobileViewport } from "./utils/mobile";
import { escapeRegExp, failOnStripeNetworkRequests, interceptCurrentRoutePost, postDataIncludesField, singleFetchActionError, skipIfUnavailable } from "./utils/navigation";
import { skipOrFailWhenE2eFixtureMissing, skipOrFailWhenE2eInputsMissing, skipWhenE2eOperatorOptInMissing } from "./utils/readiness";
const credentials = getTenantCredentials();
const dashboardPath = process.env.E2E_TENANT_DASHBOARD_PATH ?? "/app/dashboard";
const subscriptionPath = process.env.E2E_TENANT_SUBSCRIPTION_PATH ?? tenantSettingsSubscriptionPath(dashboardPath);
const subscriptionSuccessPath = process.env.E2E_TENANT_SUBSCRIPTION_SUCCESS_PATH;
const requireReadinessE2e = process.env.REQUIRE_E2E_READINESS === "1";
const requireSubscriptionSuccessE2e = process.env.E2E_REQUIRE_SUBSCRIPTION_SUCCESS === "1";
test.describe("subscription settings smoke", () => {
test.beforeEach(async ({ page }) => {
await failOnStripeNetworkRequests(page);
});
test("renders billing surface without opening Stripe redirects", async ({ page }) => {
skipOrFailWhenE2eInputsMissing(["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_SUBSCRIPTION_PATH"], "Subscription smoke inputs not configured");
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionPath, { waitUntil: "domcontentloaded" });
await skipIfUnavailable(page, response, "Subscription settings path is not reachable in this environment");
await expect(page.locator('[data-ui-id="ui.subscription.page"]')).toBeVisible();
const products = page.locator('[data-ui-id="ui.subscription.products"]');
await expect(products).toBeVisible();
if (requireReadinessE2e) {
await expect(products).toContainText("E2E Readiness Plan");
}
await expect(page.locator('[data-ui-id="ui.subscription.billing.portal"]')).toBeVisible();
const checkoutLink = page.locator('[data-ui-id="ui.subscription.checkout.link"]').first();
if ((await checkoutLink.count()) > 0) {
await expect(checkoutLink).toHaveAttribute("href", /\/subscribe\/[^/?#]+(?:[?#].*)?$/u);
await expect(checkoutLink).not.toHaveAttribute("href", /stripe\.com|^https?:\/\//u);
}
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
});
test("opens the tenant checkout catalog from subscription settings without triggering Stripe", async ({ page }) => {
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_SUBSCRIPTION_PATH"],
"Subscription checkout smoke inputs not configured"
);
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionPath, { waitUntil: "domcontentloaded" });
await skipIfUnavailable(page, response, "Subscription settings path is not reachable in this environment");
const checkoutLink = page.locator('[data-ui-id="ui.subscription.checkout.link"]').first();
if (!(await isVisible(checkoutLink))) {
skipOrFailWhenE2eFixtureMissing("No checkout catalog link rendered for this fixture");
return;
}
const checkoutHref = await checkoutLink.getAttribute("href");
expect(checkoutHref).toMatch(/^\/subscribe\/[^/?#]+(?:[?#].*)?$/u);
await Promise.all([page.waitForLoadState("domcontentloaded"), checkoutLink.click()]);
await expect(page).toHaveURL(/\/subscribe\/[^/?#]+(?:[?#].*)?$/u);
await expect(page.locator('[data-ui-id="ui.subscription.checkout.page"]').first()).toBeVisible();
const backLink = page.locator('[data-ui-id="ui.subscription.checkout.backLink"]').first();
await expect(backLink).toBeVisible();
await expect(backLink).toHaveAttribute("href", /\/settings\/subscription(?:[?#].*)?$/u);
await expect(page.getByRole("heading").first()).toBeVisible();
const checkoutButton = page.locator('[data-ui-id="ui.subscription.checkout.submit"]').first();
await expect(checkoutButton).toBeVisible();
await expect(checkoutButton).toHaveAttribute("data-product-id", /.+/u);
await expect(checkoutButton).toHaveAttribute("type", "button");
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
});
test("submits checkout catalog action contract with tenant context and CSRF", async ({ page }) => {
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_SUBSCRIPTION_PATH"],
"Subscription checkout action smoke inputs not configured"
);
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionPath, { waitUntil: "domcontentloaded" });
await skipIfUnavailable(page, response, "Subscription settings path is not reachable in this environment");
const checkoutLink = page.locator('[data-ui-id="ui.subscription.checkout.link"]').first();
if (!(await isVisible(checkoutLink))) {
skipOrFailWhenE2eFixtureMissing("No checkout catalog link rendered for this fixture");
return;
}
await Promise.all([page.waitForLoadState("domcontentloaded"), checkoutLink.click()]);
const checkoutPage = page.locator('[data-ui-id="ui.subscription.checkout.page"]').first();
// Clicking the checkout link is an SPA navigation, so waitForLoadState resolves
// immediately; wait for the /subscribe surface and an enabled submit button to
// render before the one-shot visibility guard so we don't false-trip it.
await expect(page).toHaveURL(/\/subscribe\/[^/?#]+(?:[?#].*)?$/u);
await expect(checkoutPage).toBeVisible();
const checkoutButton = checkoutPage.locator('[data-ui-id="ui.subscription.checkout.submit"]:not([disabled])').first();
await checkoutButton.waitFor({ state: "visible", timeout: 10_000 }).catch(() => undefined);
if (!(await isVisible(checkoutButton))) {
skipOrFailWhenE2eFixtureMissing("No enabled checkout submit button rendered for this fixture");
return;
}
await expect(checkoutButton).toHaveAttribute("data-product-id", /.+/u);
await expect(checkoutButton).toHaveAttribute("data-billing-period", /.+/u);
await expect(checkoutButton).toHaveAttribute("data-currency", /.+/u);
const expectedProductId = (await checkoutButton.getAttribute("data-product-id")) ?? "";
const expectedBillingPeriod = (await checkoutButton.getAttribute("data-billing-period")) ?? "";
const expectedCurrency = (await checkoutButton.getAttribute("data-currency")) ?? "";
const quantityInput = checkoutPage.locator('[data-ui-id="ui.subscription.checkout.quantity"] input[type="number"]').first();
let expectedQuantity = "1";
if (await isVisible(quantityInput)) {
await expect(quantityInput).toHaveAttribute("min", "1");
await expect(quantityInput).toHaveAttribute("max", "99");
await quantityInput.focus();
await expect(quantityInput).toBeFocused();
await quantityInput.fill("3");
expectedQuantity = "3";
}
const submittedPostData = await interceptCurrentRoutePost(page);
await checkoutButton.focus();
await expect(checkoutButton).toBeFocused();
await page.keyboard.press("Enter");
await confirmPlanChangeIfShown(page);
await expect.poll(() => submittedPostData.value, { message: "checkout catalog POST payload was captured" }).not.toBeNull();
expect(postDataIncludesField(submittedPostData.value, "action", /^subscribe$/u)).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "product-id", exactFieldValue(expectedProductId))).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "billing-period", exactFieldValue(expectedBillingPeriod))).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "currency", exactFieldValue(expectedCurrency))).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "quantity", exactFieldValue(expectedQuantity))).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "tenant-slug-context", /.+/u)).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "csrf", /.+/u)).toBe(true);
});
test("keeps checkout catalog on-page when checkout action returns an error", async ({ page }) => {
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_SUBSCRIPTION_PATH"],
"Subscription checkout error smoke inputs not configured"
);
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionPath, { waitUntil: "domcontentloaded" });
await skipIfUnavailable(page, response, "Subscription settings path is not reachable in this environment");
const checkoutLink = page.locator('[data-ui-id="ui.subscription.checkout.link"]').first();
if (!(await isVisible(checkoutLink))) {
skipOrFailWhenE2eFixtureMissing("No checkout catalog link rendered for this fixture");
return;
}
await Promise.all([page.waitForLoadState("domcontentloaded"), checkoutLink.click()]);
const checkoutPage = page.locator('[data-ui-id="ui.subscription.checkout.page"]').first();
// Clicking the checkout link is an SPA navigation, so waitForLoadState resolves
// immediately; wait for the /subscribe surface and an enabled submit button to
// render before the one-shot visibility guard so we don't false-trip it.
await expect(page).toHaveURL(/\/subscribe\/[^/?#]+(?:[?#].*)?$/u);
await expect(checkoutPage).toBeVisible();
const checkoutButton = checkoutPage.locator('[data-ui-id="ui.subscription.checkout.submit"]:not([disabled])').first();
await checkoutButton.waitFor({ state: "visible", timeout: 10_000 }).catch(() => undefined);
if (!(await isVisible(checkoutButton))) {
skipOrFailWhenE2eFixtureMissing("No enabled checkout submit button rendered for this fixture");
return;
}
await expect(checkoutButton).toHaveAttribute("data-product-id", /.+/u);
await expect(checkoutButton).toHaveAttribute("data-billing-period", /.+/u);
await expect(checkoutButton).toHaveAttribute("data-currency", /.+/u);
const expectedProductId = (await checkoutButton.getAttribute("data-product-id")) ?? "";
const expectedBillingPeriod = (await checkoutButton.getAttribute("data-billing-period")) ?? "";
const expectedCurrency = (await checkoutButton.getAttribute("data-currency")) ?? "";
const quantityInput = checkoutPage.locator('[data-ui-id="ui.subscription.checkout.quantity"] input[type="number"]').first();
let expectedQuantity = "1";
if (await isVisible(quantityInput)) {
await expect(quantityInput).toHaveAttribute("min", "1");
await expect(quantityInput).toHaveAttribute("max", "99");
await quantityInput.fill("3");
expectedQuantity = "3";
}
// The checkout submit is an RR7 single-fetch Form POST to `.data`; a raw JSON
// 400 breaks the turbo-stream decoder so actionData (and the error dialog)
// never render. Use the single-fetch error envelope helper instead.
const submittedPostData = await interceptCurrentRoutePost(page, singleFetchActionError("E2E synthetic checkout failure"));
await checkoutButton.click();
await confirmPlanChangeIfShown(page);
await expect.poll(() => submittedPostData.value, { message: "checkout catalog POST payload was captured" }).not.toBeNull();
expect(postDataIncludesField(submittedPostData.value, "action", /^subscribe$/u)).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "product-id", exactFieldValue(expectedProductId))).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "billing-period", exactFieldValue(expectedBillingPeriod))).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "currency", exactFieldValue(expectedCurrency))).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "quantity", exactFieldValue(expectedQuantity))).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "tenant-slug-context", /.+/u)).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "csrf", /.+/u)).toBe(true);
await expect(page.getByRole("dialog").filter({ hasText: "E2E synthetic checkout failure" }).first()).toBeVisible({ timeout: 10_000 });
await expect(page).toHaveURL(/\/subscribe\/[^/?#]+(?:[?#].*)?$/u);
expect(page.url()).not.toMatch(/stripe\.com/u);
});
test("renders checkout success return surface when strict operator opt-in is enabled", async ({ page }) => {
if (!requireSubscriptionSuccessE2e) {
skipWhenE2eOperatorOptInMissing(
"E2E_REQUIRE_SUBSCRIPTION_SUCCESS",
"subscription success return smoke requires an externally acquired Stripe checkout session path"
);
return;
}
if (!subscriptionSuccessPath) {
throw new Error(
"Subscription checkout success smoke requires E2E_TENANT_SUBSCRIPTION_SUCCESS_PATH while E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1"
);
}
const missing = ["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_DASHBOARD_PATH"].filter((key) => !process.env[key]);
if (missing.length > 0) {
throw new Error(
`Subscription checkout success smoke inputs not configured: missing ${missing.join(", ")} while E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1`
);
}
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD"],
"Subscription checkout success smoke inputs not configured"
);
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionSuccessPath!, { waitUntil: "domcontentloaded" });
if ([400, 403, 404].includes(response?.status() ?? 0)) {
throw new Error(
`Subscription checkout success path is not reachable in this environment: returned ${response?.status()} while E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1`
);
}
await skipIfUnavailable(page, response, "Subscription checkout success path is not reachable in this environment");
const successPage = page.locator('[data-ui-id="ui.subscription.checkout.success.page"]');
const invalidPage = page.locator('[data-ui-id="ui.subscription.checkout.success.invalid"], [data-ui-id="ui.subscription.checkout.success.error"]');
if ((await invalidPage.count()) > 0 && (await invalidPage.first().isVisible())) {
throw new Error(
"Configured checkout success path did not resolve to a valid acquired checkout session while E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1"
);
}
await expect(successPage).toBeVisible();
await expect(page.locator('[data-ui-id="ui.subscription.checkout.success.receipt"]')).toBeVisible();
await expect(page.locator('[data-ui-id="ui.subscription.checkout.success.nextSteps"]')).toBeVisible();
const dashboardLink = page.locator('[data-ui-id="ui.subscription.checkout.success.dashboardLink"]').first();
const subscriptionLink = page.locator('[data-ui-id="ui.subscription.checkout.success.subscriptionLink"]').first();
await expect(dashboardLink).toHaveAttribute("href", /\/dashboard(?:[?#].*)?$/u);
await expect(subscriptionLink).toHaveAttribute("href", /\/settings\/subscription(?:[?#].*)?$/u);
const subscriptionHref = await subscriptionLink.getAttribute("href");
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
await Promise.all([page.waitForURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")), dashboardLink.click()]);
await expectTenantDashboard(page);
expect(page.url()).not.toMatch(/stripe\.com/u);
await page.goto(subscriptionHref ?? subscriptionPath, { waitUntil: "domcontentloaded" });
await expect(page).toHaveURL(/\/settings\/subscription(?:[?#].*)?$/u);
await expectActiveSubscriptionStateAffordances(page);
expect(page.url()).not.toMatch(/stripe\.com/u);
});
test("reconciles checkout success state so it persists across reload and re-login", async ({ page }) => {
if (!requireSubscriptionSuccessE2e) {
skipWhenE2eOperatorOptInMissing(
"E2E_REQUIRE_SUBSCRIPTION_SUCCESS",
"subscription reconciliation followthrough requires an externally acquired Stripe checkout session path"
);
return;
}
if (!subscriptionSuccessPath) {
throw new Error(
"Subscription checkout reconciliation smoke requires E2E_TENANT_SUBSCRIPTION_SUCCESS_PATH while E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1"
);
}
const missing = ["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_DASHBOARD_PATH"].filter((key) => !process.env[key]);
if (missing.length > 0) {
throw new Error(
`Subscription checkout reconciliation smoke inputs not configured: missing ${missing.join(", ")} while E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1`
);
}
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD"],
"Subscription checkout reconciliation smoke inputs not configured"
);
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionSuccessPath!, { waitUntil: "domcontentloaded" });
if ([400, 403, 404].includes(response?.status() ?? 0)) {
throw new Error(
`Subscription checkout success path is not reachable in this environment: returned ${response?.status()} while E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1`
);
}
await skipIfUnavailable(page, response, "Subscription checkout success path is not reachable in this environment");
const successPage = page.locator('[data-ui-id="ui.subscription.checkout.success.page"]');
const invalidPage = page.locator('[data-ui-id="ui.subscription.checkout.success.invalid"], [data-ui-id="ui.subscription.checkout.success.error"]');
if ((await invalidPage.count()) > 0 && (await invalidPage.first().isVisible())) {
throw new Error(
"Configured checkout success path did not resolve to a valid acquired checkout session while E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1"
);
}
await expect(successPage).toBeVisible();
const dashboardLink = page.locator('[data-ui-id="ui.subscription.checkout.success.dashboardLink"]').first();
const subscriptionLink = page.locator('[data-ui-id="ui.subscription.checkout.success.subscriptionLink"]').first();
await expect(dashboardLink).toHaveAttribute("href", /\/dashboard(?:[?#].*)?$/u);
await expect(subscriptionLink).toHaveAttribute("href", /\/settings\/subscription(?:[?#].*)?$/u);
const subscriptionHref = await subscriptionLink.getAttribute("href");
// Follow the dashboard link from the checkout return surface.
await Promise.all([page.waitForURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")), dashboardLink.click()]);
await expectTenantDashboard(page);
expect(page.url()).not.toMatch(/stripe\.com/u);
// Reconciled state must be readable on the subscription settings loader and survive a fresh reload.
await page.goto(subscriptionHref ?? subscriptionPath, { waitUntil: "domcontentloaded" });
await expect(page).toHaveURL(/\/settings\/subscription(?:[?#].*)?$/u);
await expectActiveSubscriptionStateAffordances(page);
expect(page.url()).not.toMatch(/stripe\.com/u);
await page.reload({ waitUntil: "domcontentloaded" });
await expect(page).toHaveURL(/\/settings\/subscription(?:[?#].*)?$/u);
await expectActiveSubscriptionStateAffordances(page);
expect(page.url()).not.toMatch(/stripe\.com/u);
// Reconciled state must persist across a full logout + re-login (fresh appData load).
await signOutTenantUser(page);
await loginAsTenantUser(page, credentials!);
const reloginResponse = await page.goto(subscriptionHref ?? subscriptionPath, { waitUntil: "domcontentloaded" });
if ([400, 403, 404].includes(reloginResponse?.status() ?? 0)) {
throw new Error(
`Subscription settings path is not reachable after re-login: returned ${reloginResponse?.status()} while E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1`
);
}
await skipIfUnavailable(page, reloginResponse, "Subscription settings path is not reachable in this environment");
await expect(page).toHaveURL(/\/settings\/subscription(?:[?#].*)?$/u);
await expectActiveSubscriptionStateAffordances(page);
expect(page.url()).not.toMatch(/stripe\.com/u);
});
test("renders synthetic invalid checkout return fixture only when operator-enabled", async ({ page }) => {
skipWhenE2eOperatorOptInMissing(
"E2E_ALLOW_SYNTHETIC_CHECKOUT_RETURN_SURFACE",
"synthetic checkout return fixtures do not exercise the real app loader"
);
const invalidReturnPath = syntheticCheckoutReturnPath("e2e-invalid-checkout-session");
await fulfillCheckoutReturnSurface(page, invalidReturnPath, "invalid");
const response = await page.goto(invalidReturnPath, { waitUntil: "domcontentloaded" });
expect(response?.status() ?? 200).toBe(200);
await expect(page).toHaveURL(new RegExp(`${escapeRegExp(invalidReturnPath)}(?:[?#].*)?$`, "u"));
await expect(page.locator('[data-ui-id="ui.subscription.checkout.success.invalid"]')).toBeVisible();
await expect(page.locator('[data-ui-id="ui.subscription.checkout.success.subscriptionLink"]')).toHaveAttribute(
"href",
/\/settings\/subscription(?:[?#].*)?$/u
);
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
});
test("renders synthetic checkout return error fixture only when operator-enabled", async ({ page }) => {
skipWhenE2eOperatorOptInMissing(
"E2E_ALLOW_SYNTHETIC_CHECKOUT_RETURN_SURFACE",
"synthetic checkout return fixtures do not exercise the real app loader"
);
const errorReturnPath = syntheticCheckoutReturnPath("e2e-error-checkout-session");
await fulfillCheckoutReturnSurface(page, errorReturnPath, "error");
const response = await page.goto(errorReturnPath, { waitUntil: "domcontentloaded" });
expect(response?.status() ?? 200).toBe(500);
await expect(page).toHaveURL(new RegExp(`${escapeRegExp(errorReturnPath)}(?:[?#].*)?$`, "u"));
await expect(page.locator('[data-ui-id="ui.subscription.checkout.success.error"]')).toBeVisible();
await expect(page.getByText("E2E synthetic checkout return failure")).toBeVisible();
await expect(page.locator('[data-ui-id="ui.subscription.checkout.success.subscriptionLink"]')).toHaveAttribute(
"href",
/\/settings\/subscription(?:[?#].*)?$/u
);
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
});
test("submits the billing portal action with CSRF without requiring Stripe", async ({ page }) => {
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_SUBSCRIPTION_PATH"],
"Subscription billing portal smoke inputs not configured"
);
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionPath, { waitUntil: "domcontentloaded" });
await skipIfUnavailable(page, response, "Subscription settings path is not reachable in this environment");
const submittedPostData = await interceptCurrentRoutePost(page);
const portalButton = page.locator('[data-ui-id="ui.subscription.billing.portal"]').first();
await expect(portalButton).toHaveAttribute("type", "button");
await portalButton.focus();
await expect(portalButton).toBeFocused();
await page.keyboard.press("Enter");
await expect.poll(() => submittedPostData.value, { message: "billing portal POST payload was captured" }).not.toBeNull();
expect(postDataIncludesField(submittedPostData.value, "action", /^open-customer-portal$/u)).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "csrf", /.+/u)).toBe(true);
});
test("announces billing portal errors and keeps controls usable", async ({ page }) => {
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_SUBSCRIPTION_PATH"],
"Subscription billing portal error inputs not configured"
);
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionPath, { waitUntil: "domcontentloaded" });
await skipIfUnavailable(page, response, "Subscription settings path is not reachable in this environment");
const portalButton = page.locator('[data-ui-id="ui.subscription.billing.portal"]').first();
await expect(portalButton).toBeVisible();
await expect(portalButton).toHaveAttribute("type", "button");
const syntheticError = "E2E synthetic billing portal failure";
const submittedPostData = await interceptCurrentRoutePost(page, singleFetchActionError(syntheticError));
await portalButton.click();
await expect.poll(() => submittedPostData.value, { message: "billing portal error POST payload was captured" }).not.toBeNull();
expect(postDataIncludesField(submittedPostData.value, "action", /^open-customer-portal$/u)).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "csrf", /.+/u)).toBe(true);
await expect(page.getByRole("status").filter({ hasText: syntheticError }).first()).toBeVisible({ timeout: 10_000 });
await expect(page.locator('[data-ui-id="ui.subscription.page"]')).toBeVisible();
await expect(page).toHaveURL(new RegExp(`${escapeRegExp(subscriptionPath)}(?:[?#].*)?$`, "u"));
await expect(portalButton).toBeEnabled();
await portalButton.focus();
await expect(portalButton).toBeFocused();
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
expect(page.url()).not.toMatch(/stripe\.com/u);
});
test("submits cancellation action contract with CSRF when cancellation controls are rendered", async ({ page }) => {
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_SUBSCRIPTION_PATH"],
"Subscription cancellation smoke inputs not configured"
);
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionPath, { waitUntil: "domcontentloaded" });
await skipIfUnavailable(page, response, "Subscription settings path is not reachable in this environment");
const cancelButton = await findVisibleCancellationButton(page);
if (!cancelButton) {
skipOrFailWhenE2eFixtureMissing("No visible subscription cancellation controls rendered for this fixture");
return;
}
const submittedPostData = await interceptCurrentRoutePost(page);
await cancelButton.click();
const dialog = page
.locator('[role="alertdialog"]')
.filter({ has: page.locator("button") })
.last();
await expect(dialog).toBeVisible();
const confirmAction = dialog.locator('[data-ui-id="ui.confirm.action"]').first();
await expect(confirmAction).toBeVisible();
await confirmAction.click();
await expect.poll(() => submittedPostData.value, { message: "subscription cancellation POST payload was captured" }).not.toBeNull();
expect(postDataIncludesField(submittedPostData.value, "action", /^cancel$/u)).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "tenant-subscription-product-id", /.+/u)).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "csrf", /.+/u)).toBe(true);
});
test("does not submit cancellation when the confirmation dialog is dismissed", async ({ page }) => {
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_SUBSCRIPTION_PATH"],
"Subscription cancellation dismiss smoke inputs not configured"
);
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionPath, { waitUntil: "domcontentloaded" });
await skipIfUnavailable(page, response, "Subscription settings path is not reachable in this environment");
const cancelButton = await findVisibleCancellationButton(page);
if (!cancelButton) {
skipOrFailWhenE2eFixtureMissing("No visible subscription cancellation controls rendered for this fixture");
return;
}
const submittedPostData = await interceptCurrentRoutePost(page);
await cancelButton.click();
const dialog = page
.locator('[role="alertdialog"]')
.filter({ has: page.locator("button") })
.last();
await expect(dialog).toBeVisible();
const dismissAction = dialog.locator('[data-ui-id="ui.confirm.cancel"]').first();
if (await isVisible(dismissAction)) {
await dismissAction.click();
} else {
await page.keyboard.press("Escape");
}
await expect(dialog).toBeHidden();
await page.waitForTimeout(250);
expect(submittedPostData.value).toBeNull();
});
test("opens cancellation confirmation by keyboard and dismisses it with Escape without posting", async ({ page }) => {
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_SUBSCRIPTION_PATH"],
"Subscription cancellation keyboard smoke inputs not configured"
);
expect(process.env.STRIPE_SK ?? "").not.toMatch(/^sk_live_/u);
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionPath, { waitUntil: "domcontentloaded" });
await skipIfUnavailable(page, response, "Subscription settings path is not reachable in this environment");
const cancelButton = await findVisibleCancellationButton(page);
if (!cancelButton) {
skipOrFailWhenE2eFixtureMissing("No visible subscription cancellation controls rendered for this fixture");
return;
}
const submittedPostData = await interceptCurrentRoutePost(page);
await expect(cancelButton).toHaveAttribute("type", "button");
await expect(cancelButton).toBeFocused();
await page.keyboard.press("Enter");
const dialog = page.locator('[data-ui-id="ui.subscription.product.cancel.dialog"]').last();
await expect(dialog).toBeVisible();
await expect(dialog).toHaveAttribute("role", "alertdialog");
await expect(dialog.locator('[data-ui-id="ui.confirm.action"]').first()).toBeVisible();
await expectNoCriticalA11yViolations(page);
await page.keyboard.press("Escape");
await expect(dialog).toBeHidden();
await page.waitForTimeout(250);
expect(submittedPostData.value).toBeNull();
expect(page.url()).not.toMatch(/stripe\.com/u);
});
});
test.describe("subscription settings mobile smoke", () => {
test.use({ viewport: narrowMobileViewport, isMobile: true, hasTouch: true });
test.beforeEach(async ({ page }) => {
await failOnStripeNetworkRequests(page);
});
test("keeps subscription controls reachable on mobile", async ({ page }) => {
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_SUBSCRIPTION_PATH"],
"Subscription mobile smoke inputs not configured"
);
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionPath, { waitUntil: "domcontentloaded" });
await skipIfUnavailable(page, response, "Subscription settings path is not reachable in this environment");
await expect(page.locator('[data-ui-id="ui.subscription.page"]')).toBeVisible();
const products = page.locator('[data-ui-id="ui.subscription.products"]');
await expect(products).toBeVisible();
if (requireReadinessE2e) {
await expect(products).toContainText("E2E Readiness Plan");
}
const historyToggle = page.locator('[data-ui-id="ui.subscription.product.history.toggle"]').first();
if (await isVisible(historyToggle)) {
await historyToggle.focus();
await expect(historyToggle).toBeFocused();
await expect(historyToggle).toHaveAttribute("aria-expanded", "false");
await page.keyboard.press("Enter");
await expect(historyToggle).toHaveAttribute("aria-expanded", "true");
await expect(page.locator('[data-ui-id="ui.subscription.product.history"]').first()).toBeVisible();
}
await expect(page.locator('[data-ui-id="ui.subscription.billing.portal"]')).toBeVisible();
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
});
test("submits the billing portal action by touch on mobile", async ({ page }) => {
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_SUBSCRIPTION_PATH"],
"Subscription mobile billing portal inputs not configured"
);
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionPath, { waitUntil: "domcontentloaded" });
await skipIfUnavailable(page, response, "Subscription settings path is not reachable in this environment");
const submittedPostData = await interceptCurrentRoutePost(page);
const portalButton = page.locator('[data-ui-id="ui.subscription.billing.portal"]').first();
await expect(portalButton).toBeVisible();
await expect(portalButton).toHaveAttribute("type", "button");
await portalButton.tap();
await expect.poll(() => submittedPostData.value, { message: "mobile billing portal POST payload was captured" }).not.toBeNull();
expect(postDataIncludesField(submittedPostData.value, "action", /^open-customer-portal$/u)).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "csrf", /.+/u)).toBe(true);
await expectNoHorizontalOverflow(page);
});
test("follows checkout success return into the tenant dashboard on mobile when strict mode is enabled", async ({ page }) => {
if (!requireSubscriptionSuccessE2e) {
skipWhenE2eOperatorOptInMissing(
"E2E_REQUIRE_SUBSCRIPTION_SUCCESS",
"skipping strict checkout success mobile follow-through"
);
return;
}
if (!subscriptionSuccessPath) {
throw new Error(
"Subscription checkout success mobile smoke requires E2E_TENANT_SUBSCRIPTION_SUCCESS_PATH while E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1"
);
}
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_DASHBOARD_PATH"],
"Subscription checkout success mobile smoke inputs not configured"
);
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionSuccessPath, { waitUntil: "domcontentloaded" });
if ([400, 403, 404].includes(response?.status() ?? 0)) {
throw new Error(
`Subscription checkout success path is not reachable in this environment: returned ${response?.status()} while E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1`
);
}
await skipIfUnavailable(page, response, "Subscription checkout success path is not reachable in this environment");
const invalidPage = page.locator('[data-ui-id="ui.subscription.checkout.success.invalid"], [data-ui-id="ui.subscription.checkout.success.error"]');
if ((await invalidPage.count()) > 0 && (await invalidPage.first().isVisible())) {
throw new Error(
"Configured checkout success path did not resolve to a valid acquired checkout session while E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1"
);
}
await expect(page.locator('[data-ui-id="ui.subscription.checkout.success.page"]')).toBeVisible();
await expect(page.locator('[data-ui-id="ui.subscription.checkout.success.receipt"]')).toBeVisible();
const dashboardLink = page.locator('[data-ui-id="ui.subscription.checkout.success.dashboardLink"]').first();
await expect(dashboardLink).toHaveAttribute("href", /\/dashboard(?:[?#].*)?$/u);
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
await Promise.all([page.waitForURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u")), dashboardLink.tap()]);
await expectTenantDashboard(page);
expect(page.url()).not.toMatch(/stripe\.com/u);
});
});
test.describe("subscription settings compact mobile smoke", () => {
test.use({ viewport: compactMobileViewport, isMobile: true, hasTouch: true });
test.beforeEach(async ({ page }) => {
await failOnStripeNetworkRequests(page);
});
test("keeps account settings, subscription settings, and checkout catalog usable at 320px", async ({ page }) => {
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_DASHBOARD_PATH", "E2E_TENANT_SUBSCRIPTION_PATH"],
"Subscription compact mobile inputs not configured"
);
await loginAsTenantUser(page, credentials!);
const accountPath = tenantSettingsAccountPath(dashboardPath);
const accountResponse = await page.goto(accountPath, { waitUntil: "domcontentloaded" });
expect(accountResponse?.status() ?? 200).toBeLessThan(500);
await skipIfUnavailable(page, accountResponse, `Account settings path ${accountPath}`);
await expect(page).toHaveURL(new RegExp(`${escapeRegExp(accountPath)}(?:[?#].*)?$`, "u"));
await expect(page.locator('[data-ui-id="ui.settings.account.name"], form input[name="name"]').first()).toBeVisible();
await expect(page.locator('[data-ui-id="ui.settings.account.slug"], form input[name="slug"]').first()).toBeVisible();
await expect(page.locator('[data-ui-id="ui.settings.account.submit"], form button[type="submit"]').first()).toBeVisible();
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
const response = await page.goto(subscriptionPath, { waitUntil: "domcontentloaded" });
await skipIfUnavailable(page, response, "Subscription settings path is not reachable in this environment");
await expect(page.locator('[data-ui-id="ui.subscription.page"]')).toBeVisible();
await expect(page.locator('[data-ui-id="ui.subscription.products"]')).toBeVisible();
await expect(page.locator('[data-ui-id="ui.subscription.billing.portal"]')).toBeVisible();
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
const checkoutLink = page.locator('[data-ui-id="ui.subscription.checkout.link"]').first();
if (!(await isVisible(checkoutLink))) {
skipOrFailWhenE2eFixtureMissing("No checkout catalog link rendered for this fixture");
return;
}
await expect(checkoutLink).toHaveAttribute("href", /\/subscribe\/[^/?#]+(?:[?#].*)?$/u);
await Promise.all([page.waitForLoadState("domcontentloaded"), checkoutLink.tap()]);
await expect(page).toHaveURL(/\/subscribe\/[^/?#]+(?:[?#].*)?$/u);
await expect(page.locator('[data-ui-id="ui.subscription.checkout.page"]').first()).toBeVisible();
await expect(page.locator('[data-ui-id="ui.subscription.checkout.submit"]').first()).toBeVisible();
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
});
test("submits checkout catalog action contract at 320px", async ({ page }) => {
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_SUBSCRIPTION_PATH"],
"Subscription compact checkout action inputs not configured"
);
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionPath, { waitUntil: "domcontentloaded" });
await skipIfUnavailable(page, response, "Subscription settings path is not reachable in this environment");
const checkoutLink = page.locator('[data-ui-id="ui.subscription.checkout.link"]').first();
if (!(await isVisible(checkoutLink))) {
skipOrFailWhenE2eFixtureMissing("No checkout catalog link rendered for this fixture");
return;
}
await Promise.all([page.waitForLoadState("domcontentloaded"), checkoutLink.tap()]);
// SPA navigation: wait for the /subscribe surface and an enabled submit button
// before the one-shot visibility guard so we don't false-trip it.
await expect(page).toHaveURL(/\/subscribe\/[^/?#]+(?:[?#].*)?$/u);
const checkoutButton = page.locator('[data-ui-id="ui.subscription.checkout.submit"]:not([disabled])').first();
await checkoutButton.waitFor({ state: "visible", timeout: 10_000 }).catch(() => undefined);
if (!(await isVisible(checkoutButton))) {
skipOrFailWhenE2eFixtureMissing("No enabled checkout submit button rendered for this fixture");
return;
}
await expect(checkoutButton).toHaveAttribute("data-product-id", /.+/u);
await expect(checkoutButton).toHaveAttribute("data-billing-period", /.+/u);
await expect(checkoutButton).toHaveAttribute("data-currency", /.+/u);
const expectedProductId = (await checkoutButton.getAttribute("data-product-id")) ?? "";
const expectedBillingPeriod = (await checkoutButton.getAttribute("data-billing-period")) ?? "";
const expectedCurrency = (await checkoutButton.getAttribute("data-currency")) ?? "";
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
const submittedPostData = await interceptCurrentRoutePost(page);
await checkoutButton.tap();
await confirmPlanChangeIfShown(page);
await expect.poll(() => submittedPostData.value, { message: "compact checkout catalog POST payload was captured" }).not.toBeNull();
expect(postDataIncludesField(submittedPostData.value, "action", /^subscribe$/u)).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "product-id", exactFieldValue(expectedProductId))).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "billing-period", exactFieldValue(expectedBillingPeriod))).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "currency", exactFieldValue(expectedCurrency))).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "tenant-slug-context", /.+/u)).toBe(true);
expect(postDataIncludesField(submittedPostData.value, "csrf", /.+/u)).toBe(true);
});
test("keeps billing portal and cancellation controls usable at 320px", async ({ page }) => {
skipOrFailWhenE2eInputsMissing(
["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_TENANT_SUBSCRIPTION_PATH"],
"Subscription compact portal and cancellation inputs not configured"
);
await loginAsTenantUser(page, credentials!);
const response = await page.goto(subscriptionPath, { waitUntil: "domcontentloaded" });
await skipIfUnavailable(page, response, "Subscription settings path is not reachable in this environment");
await expect(page.locator('[data-ui-id="ui.subscription.page"]')).toBeVisible();
const portalButton = page.locator('[data-ui-id="ui.subscription.billing.portal"]').first();
await expect(portalButton).toBeVisible();
await expect(portalButton).toHaveAttribute("type", "button");
const cancelButton = await findVisibleCancellationButton(page);
if (!cancelButton) {
skipOrFailWhenE2eFixtureMissing("No visible subscription cancellation controls rendered for this fixture");
return;
}
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
const cancellationPostData = await interceptCurrentRoutePost(page);
await cancelButton.tap();
const dialog = page.locator('[data-ui-id="ui.subscription.product.cancel.dialog"]').last();
await expect(dialog).toBeVisible();
await expect(dialog).toHaveAttribute("role", "alertdialog");
await expect(dialog.locator('[data-ui-id="ui.confirm.action"]').first()).toBeVisible();
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
const dismissAction = dialog.locator('[data-ui-id="ui.confirm.cancel"]').first();
if (await isVisible(dismissAction)) {
await dismissAction.tap();
} else {
await page.keyboard.press("Escape");
}
await expect(dialog).toBeHidden();
await page.waitForTimeout(250);
expect(cancellationPostData.value).toBeNull();
await page.unroute("**/*");
const syntheticError = "E2E compact billing portal failure";
const portalPostData = await interceptCurrentRoutePost(page, singleFetchActionError(syntheticError));
await portalButton.tap();
await expect.poll(() => portalPostData.value, { message: "compact billing portal POST payload was captured" }).not.toBeNull();
expect(postDataIncludesField(portalPostData.value, "action", /^open-customer-portal$/u)).toBe(true);
expect(postDataIncludesField(portalPostData.value, "csrf", /.+/u)).toBe(true);
await expect(page.getByRole("status").filter({ hasText: syntheticError }).first()).toBeVisible({ timeout: 10_000 });
await expect(page.locator('[data-ui-id="ui.subscription.page"]')).toBeVisible();
await expect(portalButton).toBeEnabled();
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
expect(page.url()).not.toMatch(/stripe\.com/u);
});
});
async function isVisible(locator: Locator): Promise<boolean> {
return (await locator.count()) > 0 && (await locator.isVisible());
}
// A seeded tenant with an existing subscription routes plan upgrades through a
// proration preview modal before the real POST instead of submitting directly.
// Confirm it when present so the checkout contract capture still observes the
// "subscribe" POST; first-subscribe fixtures skip straight through.
async function confirmPlanChangeIfShown(page: Page): Promise<void> {
const confirm = page.locator('[data-ui-id="ui.subscription.planChange.confirm"]').first();
await confirm
.waitFor({ state: "visible", timeout: 2500 })
.then(() => confirm.click())
.catch(() => undefined);
}
async function findVisibleCancellationButton(page: Page): Promise<Locator | null> {
const activeProducts = page.locator('[data-ui-id="ui.subscription.product.active"]').first();
if (!(await isVisible(activeProducts))) {
return null;
}
const productCards = activeProducts.locator('[data-ui-id="ui.subscription.product.card"]');
const productCardCount = await productCards.count();
for (let index = 0; index < productCardCount; index += 1) {
const productCard = productCards.nth(index);
const button = productCard.locator('[data-ui-id="ui.subscription.product.cancel"]').first();
if ((await button.count()) > 0) {
await button.focus();
await expect(button).toBeVisible();
return button;
}
}
return null;
}
async function expectActiveSubscriptionStateAffordances(page: Page): Promise<void> {
await expect(page.locator('[data-ui-id="ui.subscription.page"]')).toBeVisible();
const products = page.locator('[data-ui-id="ui.subscription.products"]');
await expect(products).toBeVisible();
const activeProducts = products.locator('[data-ui-id="ui.subscription.product.active"]').first();
await expect(activeProducts).toBeVisible();
await expect(activeProducts.locator('[data-ui-id="ui.subscription.product.card"]').first()).toBeVisible();
const portalButton = page.locator('[data-ui-id="ui.subscription.billing.portal"]').first();
await expect(portalButton).toBeVisible();
await expect(portalButton).toHaveAttribute("type", "button");
const historyToggle = page.locator('[data-ui-id="ui.subscription.product.history.toggle"]').first();
await expect(historyToggle).toBeVisible();
await expect(historyToggle).toHaveAttribute("aria-expanded", "false");
await historyToggle.click();
await expect(historyToggle).toHaveAttribute("aria-expanded", "true");
await expect(page.locator('[data-ui-id="ui.subscription.product.history"]').first()).toBeVisible();
const cancelButton = await findVisibleCancellationButton(page);
if (!cancelButton) {
throw new Error("Expected active subscription cancellation controls after checkout success");
}
await expect(cancelButton).toHaveAttribute("type", "button");
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
}
async function signOutTenantUser(page: Page): Promise<void> {
const moreTrigger = page.locator('[data-ui-id="ui.action.shell.more"]').first();
if (await isVisible(moreTrigger)) {
await moreTrigger.click();
}
const signOut = page.locator('[data-ui-id="ui.action.shell.signOut"]').first();
await expect(signOut).toBeVisible();
await Promise.all([page.waitForLoadState("domcontentloaded"), signOut.click()]);
await expect(page).not.toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u"));
}
async function expectTenantDashboard(page: Page): Promise<void> {
await expect(page).toHaveURL(new RegExp(`${escapeRegExp(dashboardPath)}(?:[?#].*)?$`, "u"));
await expect(page).not.toHaveURL(/\/login(?:[?#].*)?$/u);
await expect(page.locator('[data-ui-id="ui.dashboard.page"]').first()).toBeVisible();
await expectNoHorizontalOverflow(page);
await expectNoCriticalA11yViolations(page);
}
function tenantSettingsSubscriptionPath(path: string): string {
const normalized = path.startsWith("/") ? path : `/${path}`;
const marker = "/dashboard";
if (!normalized.endsWith(marker)) {
return process.env.E2E_TENANT_SUBSCRIPTION_PATH ?? "/app/settings/subscription";
}
return `${normalized.slice(0, -marker.length)}/settings/subscription`;
}
function exactFieldValue(value: string): RegExp {
return new RegExp(`^${escapeRegExp(value)}$`, "u");
}
function syntheticCheckoutReturnPath(sessionId: string): string {
const subscriptionSettingsPath = tenantSettingsSubscriptionPath(dashboardPath);
const normalized = subscriptionSettingsPath.startsWith("/") ? subscriptionSettingsPath : `/${subscriptionSettingsPath}`;
const marker = "/settings/subscription";
const tenantBase = normalized.endsWith(marker) ? normalized.slice(0, -marker.length) : "/app";
const tenantSlug = tenantBase.split("/").filter(Boolean).at(-1) ?? "app";
return `/subscribe/${tenantSlug}/${sessionId}/success`;
}
async function fulfillCheckoutReturnSurface(page: Page, path: string, variant: "invalid" | "error"): Promise<void> {
const subscriptionSettingsPath = tenantSettingsSubscriptionPath(dashboardPath);
await page.route("**/*", async (route) => {
const requestUrl = new URL(route.request().url());
if (route.request().method() !== "GET" || requestUrl.pathname !== path) {
await route.fallback();
return;
}
const isError = variant === "error";
await route.fulfill({
status: isError ? 500 : 200,
contentType: "text/html",
body: `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${isError ? "Checkout return error" : "Invalid checkout session"}</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; color: #111827; background: #ffffff; }
main { min-height: 100vh; display: grid; place-items: center; padding: 24px 16px; box-sizing: border-box; }
section { width: min(100%, 520px); text-align: center; }
h1 { margin: 0; font-size: 1.875rem; line-height: 1.15; }
p { color: #b91c1c; line-height: 1.5; }
a { display: inline-flex; min-height: 44px; align-items: center; border: 1px solid #d1d5db; border-radius: 999px; padding: 0 14px; color: #111827; }
</style>
</head>
<body>
<main data-ui-id="ui.subscription.checkout.success.${variant}">
<section>
<h1>${isError ? "Unexpected error" : "Error"}</h1>
<p>${isError ? "E2E synthetic checkout return failure" : "Invalid checkout session"}</p>
<a data-ui-id="ui.subscription.checkout.success.subscriptionLink" href="${escapeHtml(subscriptionSettingsPath)}">Back to subscription</a>
</section>
</main>
</body>
</html>`,
});
});
}
function escapeHtml(value: string): string {
return value.replace(/&/gu, "&amp;").replace(/"/gu, "&quot;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;");
}