Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
167 lines
5.6 KiB
TypeScript
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 }),
|
|
})
|
|
}
|