Files
civitai__civitai/tests/preview-download.spec.ts
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

60 lines
2.9 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { storageStatePath } from './preview-fixtures';
/**
* E2E: a model detail page renders + a download affordance is present for a
* gate-passing member, on a deployed PR preview.
*
* Resolves a real model id from the PUBLIC API (`/api/v1/models`) and navigates
* DIRECTLY to `/models/<id>` — deliberately NOT browsing the heavy `/models`
* infinite-scroll listing. An earlier draft clicked the first listing card;
* loading that feed concurrently across workers crashed the single-replica
* preview pod (exit 139) and cascaded failures into the rest of the suite (see
* PR #2467 run pr-preview-2467-jpcgz). Direct navigation is light and has no
* fragile card selector.
*
* Only `gold` (a gate-passing PAID member): per the source there is no
* free-vs-paid download difference outside early-access models
* (`ModelVersionDetails.tsx`: `canDownload = version.canDownload ||
* hasDownloadPermissions`), so one member role suffices to assert "a member sees
* a download affordance". `restricted` is never used — it fails the gate.
*/
const DETAIL_URL = /\/models\/\d+(\/|$|\?)/;
test.describe('model detail + download affordance (gold)', () => {
test.use({ storageState: storageStatePath('gold') });
test('gold reaches a model detail page and sees a download affordance', async ({ page }) => {
// Resolve a real, well-formed public model id from the documented v1 API.
// page.request shares the gold session cookies; the list endpoint is public
// regardless. nsfw=false keeps us on a model without browsing-level gating.
const res = await page.request.get('/api/v1/models?limit=1&nsfw=false');
expect(res.ok(), `/api/v1/models returned HTTP ${res.status()}`).toBeTruthy();
const body = await res.json();
const modelId = body?.items?.[0]?.id;
expect(modelId, 'public API returned at least one model id').toBeTruthy();
await page.goto(`/models/${modelId}`, { waitUntil: 'domcontentloaded' });
// Cleared the gate (not bounced to either gate destination) and on a detail URL.
expect(page.url(), 'should not redirect to /login').not.toContain('/login');
expect(page.url(), 'should not redirect to /preview-restricted').not.toContain(
'/preview-restricted'
);
await expect(page).toHaveURL(DETAIL_URL);
// Detail page renders its <h1> model title.
await expect(page.getByRole('heading', { level: 1 })).toBeVisible({ timeout: 30_000 });
// A download affordance: a "Download…" button/link, or the "Download" section
// header (ModelVersionDetails.tsx). Tolerant to a size suffix / "Selected".
const downloadControl = page
.getByRole('button', { name: /download/i })
.or(page.getByRole('link', { name: /download/i }))
.or(page.getByText(/^Download( |$)/))
.first();
await expect(downloadControl).toBeVisible({ timeout: 30_000 });
});
});