Files
pn-new-crm/tests/e2e/exhaustive/13-sales-journey-mobile.spec.ts

102 lines
3.4 KiB
TypeScript
Raw Normal View History

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