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>
102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
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);
|
||
});
|
||
});
|