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); }); });