Files
pn-new-crm/tests/e2e/exhaustive/13-sales-journey-mobile.spec.ts
Matt 7591231c47
All checks were successful
Build & Push Docker Images / lint (push) Successful in 3m12s
Build & Push Docker Images / build-and-push (push) Successful in 8m24s
test(e2e): add Initiative 4 end-to-end + integration specs
Sales-process coverage (launch-readiness Initiative 4):
- exhaustive: full 7-stage sales journey + illegal-skip rejection + deposit
  total + tenancy/berth-sold; multi-berth EOI berth-range; EOI pathway parity
  (in-app vs Documenso, shared EoiContext); mobile-viewport journey.
- realapi (Documenso-gated, opt-in): generate-and-sign + post-EOI stages.
- integration: Documenso DOCUMENT_COMPLETED webhook idempotency (3x replay ->
  single file/audit write); storage backend swap (s3 <-> filesystem) with a
  real on-disk filesystem round-trip.
- visual: Reports UI snapshot cases (baselines captured separately).

1615 unit/integration pass; tsc + lint clean. Test-only change (specs are not
bundled into the app image) - no app behavior modified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:10:35 +02:00

102 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from '@playwright/test';
import { login, apiHeaders, navigateTo, PORT_SLUG } from '../smoke/helpers';
import { IPHONE_DEVICES } from '../fixtures/devices';
import { seedFullyLoadedDeal, changeStage, readStage } from '../fixtures/sales-journey';
/**
* Mobile parity for the core sales journey.
*
* Re-runs the deterministic pipeline walk at a phone viewport
* (iPhone 15/16, 393×852 — the "common" anchor in
* `tests/e2e/fixtures/devices.ts`) to confirm the stage transitions and
* the interest-detail surface behave identically on mobile. Uses the
* existing device descriptor + `test.use({ viewport, … })` rather than
* introducing a new Playwright project, since the exhaustive project
* already runs the rest of the journey on desktop.
*/
const phone = IPHONE_DEVICES.iphone16;
test.use({
viewport: phone.viewport,
deviceScaleFactor: phone.deviceScaleFactor,
isMobile: phone.isMobile,
hasTouch: phone.hasTouch,
userAgent: phone.userAgent,
});
test.describe(`exhaustive: sales journey on ${phone.name}`, () => {
test.beforeEach(async ({ page }) => {
await login(page, 'super_admin');
});
test('pipeline walk reaches contract on a mobile viewport', async ({ page }) => {
const headers = await apiHeaders(page);
const deal = await seedFullyLoadedDeal(page, 'Mobile Journey');
expect(await readStage(page.request, headers, deal.interestId)).toBe('enquiry');
for (const stage of [
'qualified',
'nurturing',
'eoi',
'reservation',
'deposit_paid',
'contract',
] as const) {
await changeStage(page.request, headers, deal.interestId, stage);
expect(
await readStage(page.request, headers, deal.interestId),
`mobile: interest should be at ${stage}`,
).toBe(stage);
}
});
test('interest detail renders its stage on a mobile viewport', async ({ page }) => {
const headers = await apiHeaders(page);
const deal = await seedFullyLoadedDeal(page, 'Mobile Detail');
await changeStage(page.request, headers, deal.interestId, 'qualified');
await navigateTo(page, `/interests/${deal.interestId}`);
await page.waitForURL(new RegExp(`/${PORT_SLUG}/interests/${deal.interestId}`), {
timeout: 10_000,
});
await page.waitForLoadState('networkidle');
// The viewport is genuinely mobile-width.
expect(page.viewportSize()?.width).toBe(phone.viewport.width);
// The stage label is present on the mobile-rendered detail page.
await expect(page.getByText(/qualified/i).first()).toBeVisible({ timeout: 10_000 });
});
test('interests list is reachable and renders on a mobile viewport', async ({ page }) => {
await navigateTo(page, '/interests');
await page.waitForLoadState('networkidle');
// The page heading is present regardless of whether the mobile layout
// shows a table or a card/board view.
await expect(page.getByText(/interests/i).first()).toBeVisible({ timeout: 15_000 });
await expect
.poll(
async () => {
const table = await page
.locator('table')
.first()
.isVisible()
.catch(() => false);
const cards = await page
.locator('[data-testid="pipeline-board"], [class*="board"], [class*="card"]')
.first()
.isVisible()
.catch(() => false);
return table || cards;
},
{ timeout: 15_000 },
)
.toBe(true);
});
});