admin/e2e/fixtures/consumerAuth.ts
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

167 lines
5.6 KiB
TypeScript

import { expect, type Page, type Route } from '@playwright/test'
export const CONSUMER_MOBILE = '09123456789'
export const CONSUMER_MOBILE_PERSIAN = '۰۹۱۲۳۴۵۶۷۸۹'
export const CONSUMER_OTP = '1234'
export const DEFAULT_BIRTH_DATE_ISO = '1991-03-21'
export const jsonOk = (data: unknown) => ({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true, data }),
})
export const installApiCatchAll = async (page: Page) => {
await page.route('**/api/v1/**', (route) =>
route.fulfill(
jsonOk({
items: [],
meta: {},
response: { page: 1, pageSize: 20, totalItemsCount: 0, totalPages: 1 },
})
)
)
// Guest home client-refetches these; catch-all object shapes crash render (.map/.filter).
await page.route('**/api/v1/discovery/cities**', (route) =>
route.fulfill(jsonOk([{ id: 1, provinceId: 1, name: 'تهران', slug: 'tehran' }]))
)
await page.route('**/api/v1/discovery/categories**', (route) => route.fulfill(jsonOk([])))
await page.route('**/api/v1/discovery/home-feed**', (route) =>
route.fulfill(jsonOk({ popular: [], categoryPreviews: [], cityPreviews: [] }))
)
}
export const createConsumerAccessToken = (status: 'active' | 'pending' = 'active') => {
const payload = Buffer.from(
JSON.stringify({
sub: 'user-id',
role: 'user',
sessionId: 'session-id',
status,
exp: Math.floor(Date.now() / 1000) + 3600,
})
).toString('base64url')
return `header.${payload}.signature`
}
export const mockRequestOtp = async (page: Page, purpose: 'login' | 'register', onRequest?: (body: { mobile?: string }) => void) => {
await page.route('**/api/v1/auth/request-otp', async (route) => {
const body = (route.request().postDataJSON() ?? {}) as { mobile?: string }
onRequest?.(body)
await route.fulfill(jsonOk({ purpose, expiresIn: 120, alreadySent: false }))
})
}
export const mockVerifyOtp = async (
page: Page,
options: {
status: 'active' | 'pending'
onRequest?: (body: Record<string, unknown>) => void
accessToken?: string
}
) => {
const accessToken = options.accessToken ?? createConsumerAccessToken(options.status)
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
await page.route('**/api/v1/auth/verify-otp', async (route) => {
const body = (route.request().postDataJSON() ?? {}) as Record<string, unknown>
options.onRequest?.(body)
await route.fulfill(
jsonOk({
accessToken,
sessionId: 'session-id',
expiresAt,
status: options.status,
})
)
})
return accessToken
}
export const mockCities = async (page: Page) => {
await page.route('**/api/v1/geography/cities', (route) =>
route.fulfill(jsonOk([{ id: 1, provinceId: 1, name: 'تهران', slug: 'tehran' }]))
)
}
export const mockCompleteProfile = async (page: Page, onRequest?: (body: Record<string, unknown>) => void) => {
await page.route('**/api/v1/users/me/complete-profile', async (route) => {
const body = (route.request().postDataJSON() ?? {}) as Record<string, unknown>
onRequest?.(body)
await route.fulfill(
jsonOk({
id: 'user-id',
firstName: 'علی',
lastName: 'رضایی',
gender: 'male',
status: 'active',
cityId: 1,
})
)
})
}
/** AuthGate consumer mobile field has no label — only the placeholder. */
export const mobileField = (page: Page) => page.getByPlaceholder('مثال ۰۹۱۲۳۴۵۶۷۸۹')
export const openAuthGateFromHome = async (page: Page) => {
await page.goto('/?auth=1', { waitUntil: 'domcontentloaded' })
await expect(page.getByRole('heading', { name: 'سلام!' })).toBeVisible({ timeout: 15_000 })
await expect(mobileField(page)).toBeVisible()
}
export const submitMobile = async (page: Page, mobile = CONSUMER_MOBILE_PERSIAN) => {
await mobileField(page).fill(mobile)
await page.getByRole('button', { name: 'ادامه' }).click()
}
export const fillOtpCode = async (page: Page, code = CONSUMER_OTP) => {
await page.locator('input').first().fill(code)
}
export const completeConsumerProfileForm = async (page: Page) => {
await page.getByText('مرد', { exact: true }).click()
await page.getByPlaceholder('نام', { exact: true }).fill(' علی ')
await page.getByPlaceholder('نام خانوادگی', { exact: true }).fill(' رضایی ')
await page.getByRole('button', { name: 'شهر' }).click()
const cityPicker = page.getByRole('dialog').filter({ hasText: 'شهرتان را انتخاب کنید' })
await expect(cityPicker).toBeVisible()
await cityPicker.getByRole('button', { name: 'تهران' }).click()
await cityPicker.getByRole('button', { name: 'انتخاب' }).click()
await page.getByRole('button', { name: 'تاریخ تولد' }).click()
const birthPicker = page.getByRole('dialog').filter({ hasText: 'تاریخ تولدتان را انتخاب کنید' })
await expect(birthPicker).toBeVisible()
await birthPicker.getByRole('button', { name: 'انتخاب' }).click()
await page.getByRole('button', { name: 'تکمیل ثبت‌نام' }).click()
}
export const expectActiveSession = async (page: Page) => {
const storedUser = await page.evaluate(() => JSON.parse(localStorage.getItem('user') ?? 'null'))
expect(storedUser).toMatchObject({
userId: 'user-id',
role: 'user',
sessionId: 'session-id',
status: 'active',
})
expect(storedUser.refreshToken).toBeUndefined()
}
export const fulfillApiError = async (route: Route, code: string, message: string, status = 403) => {
await route.fulfill({
status,
contentType: 'application/json',
body: JSON.stringify({ success: false, code, message }),
})
}