Ctrl K
ComunidadGuías…

tests/team-billing-cancel.spec.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { expect, type Page } from "@playwright/test";

import { Account, Subscription } from "../apps/backend/src/database/models";
import { loggedTest } from "./logged-test";
import { ensureTeamOwner, screenshot } from "./util";

loggedTest.beforeEach(async ({ auth, team }) => {
  await ensureTeamOwner({ team: team.team, user: auth.user });
});

/**
 * Capture the page with the survey open, scrolled back to the top first.
 *
 * The dialog and its backdrop are fixed, so a full-page capture draws them
 * wherever the page was scrolled. Opening the survey scrolls down to the cancel
 * card at the bottom of Billing, and picking an option while the list is still
 * animating in makes Playwright retry with a different alignment each time, so
 * the offset changes from run to run.
 */
async function screenshotSurvey(page: Page, name: string) {
  await page.evaluate(() => window.scrollTo(0, 0));
  await screenshot(page, name);
}

/**
 * Put the team on a real Stripe subscription.
 *
 * The fixture team is on a forced plan, which the plan card reads as a contract
 * we bill by hand — so the cancellation controls only appear once Stripe is
 * actually behind the plan.
 */
async function subscribeToStripe(input: {
  accountId: string;
  planId: string;
  subscriberId: string;
  slug: string;
  /** Seeds a running trial instead of a paid subscription. */
  trial?: boolean;
}) {
  await Account.query()
    .findById(input.accountId)
    .patch({ forcedPlanId: null, stripeCustomerId: `cus_${input.slug}` });
  await Subscription.query().insert({
    planId: input.planId,
    accountId: input.accountId,
    provider: "stripe",
    stripeSubscriptionId: `sub_${input.slug}`,
    subscriberId: input.subscriberId,
    startDate: new Date(Date.now() - 30 * 24 * 3600 * 1000).toISOString(),
    endDate: null,
    // A trial prices its period off `trialEndDate`, so the card needs one.
    trialEndDate: input.trial
      ? new Date(Date.now() + 10 * 24 * 3600 * 1000).toISOString()
      : null,
    paymentMethodFilled: !input.trial,
    status: input.trial ? "trialing" : "active",
  });
}

loggedTest(
  "team billing - cancelation asks why, and offers a call on price",
  async ({ page, team, auth, plan }) => {
    await subscribeToStripe({
      accountId: team.account.id,
      planId: plan.id,
      subscriberId: auth.user.id,
      slug: team.account.slug,
    });

    await page.goto(`/${team.account.slug}/settings/billing`);
    await page.getByRole("button", { name: "Cancel subscription" }).click();

    const dialog = page.getByRole("dialog");
    await expect(
      dialog.getByRole("heading", { name: "Cancel subscription" }),
    ).toBeVisible();
    await screenshotSurvey(page, "cancel-subscription-survey");

    // Neither picked nor written: the survey asks again rather than letting the
    // cancelation through with no reason at all.
    await dialog.getByRole("button", { name: "Cancel subscription" }).click();
    await expect(
      dialog.getByText("Tell us why, from the list or in your own words."),
    ).toBeVisible();

    // "Other" is the one choice that cannot stand on its own.
    await dialog.getByLabel("Why are you leaving?").click();
    await page.getByRole("option", { name: "Other", exact: true }).click();
    await dialog.getByRole("button", { name: "Cancel subscription" }).click();
    await expect(
      dialog.getByText("Tell us what went wrong so we can fix it."),
    ).toBeVisible();

    await dialog.getByLabel("Why are you leaving?").click();
    await page.getByRole("option", { name: "It costs too much" }).click();

    // Price is the one answer that opens an offer, and it opens in place: no
    // extra screen between the leaver and the way out.
    await expect(dialog.getByText("Before you go")).toBeVisible();
    await expect(
      dialog.getByText("The price is usually something we can work on"),
    ).toBeVisible();
    await expect(
      dialog.getByRole("link", { name: "book a 15-minute call" }),
    ).toHaveCount(1);
    await screenshotSurvey(page, "cancel-subscription-price-offer");
  },
);

loggedTest(
  "team billing - a scheduled cancelation can be called off",
  async ({ page, team, auth, plan }) => {
    await subscribeToStripe({
      accountId: team.account.id,
      planId: plan.id,
      subscriberId: auth.user.id,
      slug: team.account.slug,
    });
    await Subscription.query()
      .patch({
        endDate: new Date(Date.now() + 20 * 24 * 3600 * 1000).toISOString(),
      })
      .where({ accountId: team.account.id });

    await page.goto(`/${team.account.slug}/settings/billing`);

    // The card announces the date the plan stops on instead of a payment that
    // will never be taken.
    await expect(
      page.getByText("Your subscription is canceled."),
    ).toBeVisible();
    // The cancellation card turns into the way back, and the banner offers
    // the same action under the same name: the portal no longer can.
    await expect(
      page.getByRole("button", { name: "Resume subscription" }),
    ).toHaveCount(2);
    await expect(
      page.getByRole("button", { name: "Cancel subscription" }),
    ).toHaveCount(0);
  },
);

loggedTest(
  "team billing - a trial is never told it already paid",
  async ({ page, team, auth, plan }) => {
    await subscribeToStripe({
      accountId: team.account.id,
      planId: plan.id,
      subscriberId: auth.user.id,
      slug: team.account.slug,
      trial: true,
    });

    // A trial with no card ends on its own: there is nothing to cancel.
    await page.goto(`/${team.account.slug}/settings/billing`);
    await expect(
      page.getByRole("button", { name: "Add payment method" }).first(),
    ).toBeVisible();
    await expect(page.getByRole("button", { name: /^Cancel/ })).toHaveCount(0);

    await Subscription.query()
      .patch({ paymentMethodFilled: true })
      .where({ accountId: team.account.id });
    await page.reload();
    await page.getByRole("button", { name: "Cancel trial" }).click();

    const dialog = page.getByRole("dialog");
    await expect(dialog.getByText("until the trial ends")).toBeVisible();
    await expect(dialog.getByText("already paid for")).toHaveCount(0);
  },
);