Ctrl K
ComunidadGuías…

tests/pages.spec.ts

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

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

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

/**
 * The pages no other spec captures, each once, so that a change to their
 * layout shows in Argos, at a phone's width too (see `screenshot`). A few
 * pages per test: enough to share a viewer, few enough to run in parallel.
 */

type Viewer = { account: { slug: string }; user: { email: string | null } };

/**
 * The viewer's slug and email, and a team's slug, are unique per test, so
 * Argos is shown fixed ones. Only those the page shows: `screenshot` fails on
 * a text it cannot find.
 */
async function maskViewer(
  page: Page,
  auth: Viewer,
  others: Record<string, string> = {},
) {
  const candidates: Record<string, string> = {
    ...others,
    [auth.account.slug]: "kyle",
  };
  if (auth.user.email) {
    candidates[auth.user.email] = "kyle@acme.com";
  }
  const replacements: Record<string, string> = {};
  // Longest first: the email holds the slug, so the slug must still show once
  // the email is replaced, outside of it.
  const searches = Object.keys(candidates).sort((a, b) => b.length - a.length);
  for (const [index, search] of searches.entries()) {
    const outside = searches
      .slice(0, index)
      .filter((longer) => longer.startsWith(search))
      .map((longer) => `(?!${escapeRegExp(longer.slice(search.length))})`)
      .join("");
    const text = new RegExp(`${escapeRegExp(search)}${outside}`);
    if (await page.getByText(text).first().isVisible()) {
      replacements[search] = candidates[search] as string;
    }
  }
  return { replacements };
}

function escapeRegExp(text: string) {
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

async function visit(page: Page, path: string, ready: () => Promise<void>) {
  await page.goto(path);
  await ready();
}

loggedTest("personal projects and settings", async ({ page, auth }) => {
  const slug = auth.account.slug;
  await visit(page, `/${slug}`, () =>
    expect(
      page.getByRole("heading", { name: "Create your first project" }),
    ).toBeVisible(),
  );
  await screenshot(page, "personal-projects", await maskViewer(page, auth));

  await visit(page, `/${slug}/settings`, () =>
    expect(
      page.getByRole("heading", { name: "Personal Settings" }),
    ).toBeVisible(),
  );
  // An input's value is not text the replacements reach.
  await page
    .getByRole("region", { name: "Your Username" })
    .getByRole("textbox", { name: "URL namespace", exact: true })
    .fill("kyle");
  await screenshot(
    page,
    "personal-settings-general",
    await maskViewer(page, auth),
  );

  await visit(page, `/${slug}/settings/authentication`, () =>
    expect(page.getByText("Sessions", { exact: true })).toBeVisible(),
  );
  await screenshot(
    page,
    "personal-settings-authentication",
    await maskViewer(page, auth),
  );
});

loggedTest("personal settings sections", async ({ page, auth }) => {
  const slug = auth.account.slug;
  const sections = [
    { id: "billing", ready: () => page.getByRole("heading", { name: "Plan" }) },
    {
      id: "notifications",
      ready: () => page.getByText("Notifications", { exact: true }).last(),
    },
    {
      id: "tokens",
      // The section's link reads the same: the card's title comes after it.
      ready: () => page.getByText("Tokens", { exact: true }).last(),
    },
    {
      id: "applications",
      ready: () => page.getByText("Authorized applications"),
    },
  ];
  for (const section of sections) {
    await visit(page, `/${slug}/settings/${section.id}`, () =>
      expect(section.ready()).toBeVisible(),
    );
    await screenshot(
      page,
      `personal-settings-${section.id}`,
      await maskViewer(page, auth),
    );
  }
});

loggedTest("teams, new team and welcome", async ({ page, auth }) => {
  await visit(page, "/teams", () => expect(page).toHaveTitle(/Teams/));
  await screenshot(page, "teams", await maskViewer(page, auth));

  await visit(page, "/teams/new", () =>
    expect(page.getByRole("heading", { name: "Create a Team" })).toBeVisible(),
  );
  await screenshot(page, "new-team", await maskViewer(page, auth));

  await visit(page, "/~/welcome", () =>
    expect(
      page.getByRole("heading", { name: "Welcome to Argos" }),
    ).toBeVisible(),
  );
  await screenshot(page, "welcome", await maskViewer(page, auth));
});

loggedTest("links from emails", async ({ page, auth }) => {
  await visit(page, "/verify", () =>
    expect(page.getByText("Invalid verification link")).toBeVisible(),
  );
  await screenshot(page, "verify-email-invalid", await maskViewer(page, auth));

  await visit(page, "/account/delete?token=a-deletion-token", () =>
    expect(page).toHaveTitle(/Confirm Account Deletion/),
  );
  await screenshot(
    page,
    "account-delete-confirm",
    await maskViewer(page, auth),
  );

  await visit(page, "/this/page/does/not/exist", () =>
    expect(page.getByRole("heading", { name: "Page not found" })).toBeVisible(),
  );
  await screenshot(page, "not-found", await maskViewer(page, auth));
});

loggedTest("team invites", async ({ page, auth, team }, testInfo) => {
  // Not a member yet: a member is sent straight to the team.
  const mask = () => maskViewer(page, auth, { [team.account.slug]: "acme" });

  // The team's own link, shared by hand.
  const link = new URL(await team.team.$getInviteLink());
  await visit(page, link.pathname, () => expect(page).toHaveTitle(/Join/));
  await expect(page.getByRole("button", { name: /Join/ })).toBeVisible();
  await screenshot(page, "team-invite-link", await mask());

  // An invitation sent by email, only valid for the address it went to.
  const secret =
    `${getUniqueTestIdentifier(testInfo)}aaaaaaaaaaaaaaaaaaaa`.slice(0, 20);
  await TeamInvite.query().insert({
    email: auth.user.email ?? "kyle@acme.com",
    teamId: team.team.id,
    userLevel: "member",
    invitedById: auth.user.id,
    secret,
    expiresAt: new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString(),
  });
  await visit(page, `/invites/${secret}`, () =>
    expect(page).toHaveTitle(/Join/),
  );
  await expect(page.getByRole("button", { name: /Join|Accept/ })).toBeVisible();
  await screenshot(page, "team-invite-email", await mask());
});