Files
Zachary Lowden d3ec4748dd test(preview): buzz / membership / download e2e specs (#2467)
* test(preview): buzz / membership / download e2e specs

Extends the preview smoke harness with the first tranche of flow coverage
(revenue/access surfaces), run report-only against deployed previews as the
minted fixture users:

- preview-buzz.spec.ts: /purchase/buzz + /pricing render package/plan options
  and a checkout affordance for a gate-passing paid member (gold); no transact.
- preview-membership.spec.ts: gold (paid) sees the current-member view;
  tester (free) is SSR-redirected to /pricing and sees a subscribe CTA. Keys on
  the deterministic getServerSideProps redirect, not copy.
- preview-download.spec.ts: gold + tester reach a model detail page from the
  listing (no hardcoded id) and see a download affordance. (restricted is NOT
  used — it fails the gate; the in-app comparison is free vs paid, and the code
  has no guaranteed free/paid download difference outside early-access.)

Also anchor the preview-config testMatch to the filename
(/(^|\/)preview-.*\.spec\.ts$/) — the unanchored form matched a parent dir
named like a worktree (civitai-preview-*) and wrongly pulled the non-preview
specs (auction/generation/example) into the preview run.

Selectors are first-draft (broad/resilient) and get validated by the next
report-only preview run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(preview): make download spec light + robust (was crashing the preview)

The first draft browsed the heavy /models infinite-scroll listing and clicked
the first card, for both gold AND tester across 4 workers. That load crashed the
single-replica preview pod (exit 139) and cascaded fast failures into the rest
of the suite (base smoke went red as collateral on PR #2467 run jpcgz).

Rework: resolve a real model id from the public /api/v1/models API and navigate
directly to /models/<id> — no listing, no fragile card selector — and assert as
gold only (no free-vs-paid download difference exists outside early-access).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(preview): run smoke suite serially (workers:1)

The expanded 19-test suite at workers:4 segfaulted the single-replica preview
pod (exit 139) under concurrent heavy-page loads, cascading fast failures into
unrelated tests (base smoke loads went red on PR #2467 run s97wq even after the
download spec was lightened). The pod can't take concurrent load; run one page
at a time. Suite is small + report-only, so serial (~2-3 min) is the right call.
Reverses the earlier workers:4 — that suited a 12-test light suite, not this.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: rebuild preview to validate heap fix (2Gi/1536)

* test(preview): authed warm-up of heavy pages + 60s nav timeout

Mop up the last cold-load flake (`tester loads /models`) now that the pod OOM
is fixed. Two changes:
- Warm /models + /images (not just /) in setup, AUTHENTICATED with the gold
  cookie — an anon GET would just hit the gate's /login redirect and never warm
  the real listing render path.
- navigationTimeout 45s -> 60s, aligned with the per-test timeout, so a cold
  /models goto gets the full budget instead of dying at 45s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:30:28 -05:00

115 lines
6.0 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { storageStatePath } from './preview-fixtures';
/**
* Buzz purchase + membership pricing entry-point tests for a deployed PR preview.
*
* Runs as the `gold` fixture (a gate-passing PAID member with Buzz) so we exercise
* the real, prod-clone-backed purchase/pricing surfaces rather than the gate
* redirect. We assert STRUCTURE only — the page loads behind the gate, shows
* package/plan choices, and exposes a reachable purchase/checkout affordance — and
* deliberately never transact (no click into a live Stripe/Paddle/crypto form).
*
* Both surfaces branch on the runtime "buzz type" (green vs. yellow domain) and on
* feature flags, so the assertions below are written to tolerate EITHER branch:
* - /purchase/buzz renders <BuzzPurchaseLayout>, which is either the package grid
* (<BuzzPurchaseImproved>, green) OR the crypto-deposit tab + card upsell
* (yellow). The sidebar <BuzzFeatures> with the fixed title "What can you do
* with Buzz?" renders in BOTH branches, so it's the stable page-loaded anchor.
* - /pricing renders <MembershipPageWrapper>, whose <Meta> title is always
* "Memberships | Civitai", with either the <MembershipPlans> cards (green) or
* the YellowMembershipUnavailable cards (yellow) inside.
*
* Only runs under playwright.preview.config.ts (needs PREVIEW_URL + minted states).
*/
const ROLE = 'gold' as const;
// Assert we cleared the preview gate (not bounced to /login or /preview-restricted)
// and got a real (<400) response — mirrors preview-smoke.spec.ts.
function assertGatePassed(page: import('@playwright/test').Page, path: string) {
expect(page.url(), `${path}: should not redirect to /login`).not.toContain('/login');
expect(page.url(), `${path}: should not redirect to /preview-restricted`).not.toContain(
'/preview-restricted'
);
}
test.describe('buzz purchase + pricing entry (gold)', () => {
test.use({ storageState: storageStatePath(ROLE) });
test('/purchase/buzz renders package/purchase options and a checkout affordance', async ({
page,
}) => {
const resp = await page.goto('/purchase/buzz', { waitUntil: 'domcontentloaded' });
expect(resp?.status(), 'HTTP status for /purchase/buzz').toBeLessThan(400);
assertGatePassed(page, '/purchase/buzz');
// Page-loaded anchor present in BOTH purchase branches: the BuzzFeatures
// sidebar is rendered by BuzzPurchaseLayout with this exact, hard-coded title.
// NOTE: matched on the literal "What can you do with Buzz?" passed in
// BuzzPurchaseLayout.tsx. If the heading copy changes, the per-branch
// anchors below (Choose Your Package / Pay with a card / deposit) still gate.
await expect(
page.getByText('What can you do with Buzz?', { exact: false }).first()
).toBeVisible({ timeout: 30_000 });
// A reachable purchase/checkout affordance exists. We don't assert WHICH
// branch rendered (green package grid vs. yellow crypto + card upsell), only
// that at least one known purchase control/CTA is on the page. Matched
// controls, by branch:
// green -> "Choose Your Package" heading + "Complete Purchase" / "Pay with Card"
// yellow -> NoCryptoUpsell "Prefer to pay with a card?" + "Buy Green Buzz",
// plus the crypto DepositAddressCard.
// NOTE: this is a broad OR over stable copy strings rather than a CSS/testid
// selector (none exists on these controls yet). If a preview run shows a
// different label, widen this regex — the structural intent is "some
// purchase/checkout entry is present".
const purchaseAffordance = page
.getByText(
/Choose Your Package|Complete Purchase|Pay with Card|Prefer to pay with a card|Buy Green Buzz|Custom Amount|deposit/i
)
.first();
await expect(purchaseAffordance).toBeVisible({ timeout: 30_000 });
});
test('/pricing renders membership plan options', async ({ page }) => {
const resp = await page.goto('/pricing', { waitUntil: 'domcontentloaded' });
expect(resp?.status(), 'HTTP status for /pricing').toBeLessThan(400);
assertGatePassed(page, '/pricing');
// Always-present anchor: MembershipPageWrapper renders <Meta title=
// "Memberships | Civitai"> regardless of branch (green plans vs. yellow
// "memberships unavailable" view). The document <title> is the most stable
// signal that the membership page (not an error/redirect) rendered.
// NOTE: a title check is resilient to the body branching; the body-text
// assertion below is the structural "plan options present" check.
await expect(page).toHaveTitle(/Memberships/i, { timeout: 30_000 });
// At least one membership plan option / CTA is present. Matched copy by branch:
// green -> <MembershipPlans> cards; tiers + "Memberships" heading.
// yellow -> YellowMembershipUnavailable: "Green Membership" / "Purchase Buzz"
// cards with "View Green Memberships" / "Go to Buzz Purchase" CTAs.
// NOTE: broad OR over stable plan-related copy; widen if a preview shows
// different tier labels. Asserts structure (a plan choice exists), never a
// specific price/count.
const planOption = page
.getByText(
/Membership|Memberships|View Green Memberships|Go to Buzz Purchase|Supporter|Bronze|Silver|Gold/i
)
.first();
await expect(planOption).toBeVisible({ timeout: 30_000 });
});
test('/purchase/buzz and /pricing both stay behind the gate (no bounce)', async ({ page }) => {
// Focused regression for the gate contract on these two paid surfaces:
// a paid member must NOT be redirected to /login or /preview-restricted, and
// both must return a real (<400) response. Kept separate from the rich-content
// assertions above so a copy/selector drift can't mask a gate/redirect break.
for (const path of ['/purchase/buzz', '/pricing']) {
const resp = await page.goto(path, { waitUntil: 'domcontentloaded' });
expect(resp?.status(), `HTTP status for ${path}`).toBeLessThan(400);
assertGatePassed(page, path);
}
});
});