Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 

552 rindas
23 KiB

  1. import type { Locator, Page } from '@playwright/test'
  2. import { captureScreenshot, expect, test } from './fixtures'
  3. const loginContext = {
  4. defaultRoleCode: 'admin',
  5. roles: [
  6. {
  7. code: 'administrative',
  8. label: '行政',
  9. accountLabel: '教学系领导账号',
  10. organizationMode: 'SINGLE',
  11. organizationLabels: ['教学系'],
  12. },
  13. {
  14. code: 'teacher',
  15. label: '教员',
  16. accountLabel: '教员账号',
  17. organizationMode: 'CASCADE',
  18. organizationLabels: ['教学系', '教研室'],
  19. },
  20. {
  21. code: 'student',
  22. label: '学员',
  23. accountLabel: '学号或账号',
  24. organizationMode: 'NONE',
  25. organizationLabels: [],
  26. },
  27. {
  28. code: 'admin',
  29. label: '管理员',
  30. accountLabel: '管理员账号',
  31. organizationMode: 'NONE',
  32. organizationLabels: [],
  33. },
  34. ],
  35. departments: [
  36. {
  37. id: '100',
  38. code: 'EQUIPMENT',
  39. name: '工程装备系',
  40. parentId: null,
  41. children: [
  42. {
  43. id: '110',
  44. code: 'MAINTENANCE',
  45. name: '维修教研室',
  46. parentId: '100',
  47. children: [
  48. {
  49. id: '111',
  50. code: 'MAINTENANCE-GROUP-1',
  51. name: '维修一组',
  52. parentId: '110',
  53. children: [
  54. {
  55. id: '112',
  56. code: 'HYDROPOWER-A',
  57. name: '水电组A',
  58. parentId: '111',
  59. children: [],
  60. },
  61. ],
  62. },
  63. ],
  64. },
  65. {
  66. id: '120',
  67. code: 'SUPPORT',
  68. name: '智能保障教研室',
  69. parentId: '100',
  70. children: [],
  71. },
  72. ],
  73. },
  74. {
  75. id: '200',
  76. code: 'COMMAND',
  77. name: '指挥系',
  78. parentId: null,
  79. children: [
  80. {
  81. id: '210',
  82. code: 'COMMAND-TEACHING',
  83. name: '指挥教研室',
  84. parentId: '200',
  85. children: [],
  86. },
  87. ],
  88. },
  89. ],
  90. } as const
  91. const loginIdentities = {
  92. administrative: [
  93. { id: '301', displayName: '秦主任', departmentId: '100', departmentName: '工程装备系' },
  94. { id: '302', displayName: '周主任', departmentId: '200', departmentName: '指挥系' },
  95. ],
  96. teacher: [
  97. { id: '401', displayName: '许教员', departmentId: '100', departmentName: '工程装备系' },
  98. { id: '402', displayName: '杜晴', departmentId: '110', departmentName: '维修教研室' },
  99. { id: '403', displayName: '马雅柔', departmentId: '112', departmentName: '水电组A' },
  100. { id: '404', displayName: '韩教员', departmentId: '120', departmentName: '智能保障教研室' },
  101. ],
  102. student: [
  103. { id: '501', displayName: '学生甲', departmentId: '112', departmentName: '水电组A' },
  104. ],
  105. } as const
  106. const descendantDepartmentIds: Record<string, ReadonlySet<string>> = {
  107. '100': new Set(['100', '110', '111', '112', '120']),
  108. '110': new Set(['110', '111', '112']),
  109. '120': new Set(['120']),
  110. '200': new Set(['200', '210']),
  111. '210': new Set(['210']),
  112. }
  113. const mockLoginContext = async (page: Page) => {
  114. await page.route('**/api/auth/v1/auth/login-context', async (route) => {
  115. await route.fulfill({
  116. status: 200,
  117. contentType: 'application/json',
  118. body: JSON.stringify({
  119. code: 200,
  120. message: '成功',
  121. data: loginContext,
  122. timestamp: '2026-08-11T00:00:00+08:00',
  123. requestId: 'ute2e-login-context-success',
  124. }),
  125. })
  126. })
  127. await page.route('**/api/auth/v1/auth/login-identities**', async (route) => {
  128. const url = new URL(route.request().url())
  129. const roleCode = url.searchParams.get('roleCode')
  130. const departmentId = url.searchParams.get('departmentId')
  131. const source = roleCode && roleCode in loginIdentities
  132. ? loginIdentities[roleCode as keyof typeof loginIdentities]
  133. : []
  134. const allowedDepartmentIds = departmentId ? descendantDepartmentIds[departmentId] : null
  135. const identities = source.filter((identity) => !allowedDepartmentIds || allowedDepartmentIds.has(identity.departmentId))
  136. await route.fulfill({
  137. status: 200,
  138. contentType: 'application/json',
  139. body: JSON.stringify({
  140. code: 200,
  141. message: '成功',
  142. data: identities,
  143. timestamp: '2026-08-11T00:00:00+08:00',
  144. requestId: 'ute2e-login-identities-success',
  145. }),
  146. })
  147. })
  148. }
  149. const mockLoginContextFailure = async (page: Page) => {
  150. await page.route('**/api/auth/v1/auth/login-context', async (route) => {
  151. await route.fulfill({
  152. status: 503,
  153. contentType: 'application/json',
  154. body: JSON.stringify({
  155. code: 50300,
  156. message: '认证配置暂不可用',
  157. data: null,
  158. timestamp: '2026-08-11T00:00:00+08:00',
  159. requestId: 'ute2e-login-context-failure',
  160. }),
  161. })
  162. })
  163. }
  164. const openLoginWithContext = async (page: Page) => {
  165. await mockLoginContext(page)
  166. await page.goto('/login')
  167. const contextState = page.getByText('身份认证服务正常', { exact: true })
  168. await expect(contextState).toBeAttached()
  169. await expect(contextState).toHaveAttribute('aria-live', 'polite')
  170. await expect(contextState).toHaveCSS('position', 'absolute')
  171. await expect(contextState).toHaveCSS('clip-path', 'inset(50%)')
  172. }
  173. const adminUsernameInput = (page: Page) => page.locator('input[name="username"]')
  174. const passwordInput = (page: Page) => page.locator('input[name="password"]')
  175. const submitButton = (page: Page) => page.getByRole('button', { name: '登录系统' })
  176. const identitySelect = (page: Page) => page.getByTestId('login-identity')
  177. const openElementSelect = async (page: Page, select: Locator) => {
  178. const combobox = select.getByRole('combobox')
  179. await select.locator('.el-select__wrapper').click()
  180. await expect(combobox).toHaveAttribute('aria-expanded', 'true')
  181. const listboxId = await combobox.getAttribute('aria-controls')
  182. expect(listboxId).toBeTruthy()
  183. const listbox = page.locator(`[id="${listboxId}"]`)
  184. await expect(listbox).toBeVisible()
  185. return listbox
  186. }
  187. const selectElementOption = async (page: Page, select: Locator, name: string | RegExp) => {
  188. const listbox = await openElementSelect(page, select)
  189. await listbox.getByRole('option', { name, exact: typeof name === 'string' }).click()
  190. }
  191. const expectElementSelectOptions = async (page: Page, select: Locator, expected: string[]) => {
  192. const listbox = await openElementSelect(page, select)
  193. const labels = (await listbox.getByRole('option').allTextContents())
  194. .map((label) => label.replace(/\s+/g, ' ').trim())
  195. expect(labels).toEqual(expected)
  196. await page.keyboard.press('Escape')
  197. }
  198. const expectElementSelectPlaceholder = async (select: Locator, placeholder: string) => {
  199. await expect(select.locator('.el-select__placeholder').first()).toContainText(placeholder)
  200. }
  201. test.describe('登录上下文 UI 契约', () => {
  202. test('恢复上次成功登录的部门和账号但不保存密码', async ({ page }, testInfo) => {
  203. await page.addInitScript(() => {
  204. window.localStorage.setItem('unreal-tran:web:login-identity-preference:v1', JSON.stringify({
  205. lastRoleCode: 'teacher',
  206. selections: {
  207. teacher: { departmentId: '110', userId: '402', username: '' },
  208. },
  209. }))
  210. })
  211. await openLoginWithContext(page)
  212. await expect(page.locator('[data-role-code="teacher"]')).toHaveAttribute('aria-pressed', 'true')
  213. await expect(page.getByTestId('login-organization')).toContainText('工程装备系')
  214. await expect(page.getByTestId('login-department')).toContainText('维修教研室')
  215. await expect(identitySelect(page)).toContainText('杜晴')
  216. await expect(passwordInput(page)).toHaveValue('')
  217. await expect(submitButton(page)).toBeDisabled()
  218. await captureScreenshot(page, testInfo, 'restored-login-identity-without-password')
  219. })
  220. test('按登录身份分别恢复记住的账号和密码', async ({ page }) => {
  221. await page.addInitScript(() => {
  222. window.localStorage.setItem('unreal-tran:web:login-identity-preference:v1', JSON.stringify({
  223. lastRoleCode: 'teacher',
  224. selections: {
  225. teacher: {
  226. departmentId: '110', userId: '402', username: '',
  227. password: 'Teacher-Remembered-1', remember: true,
  228. },
  229. admin: {
  230. departmentId: '', userId: '', username: 'admin.remembered',
  231. password: 'Admin-Remembered-2', remember: true,
  232. },
  233. },
  234. }))
  235. })
  236. await openLoginWithContext(page)
  237. await expect(identitySelect(page)).toContainText('杜晴')
  238. await expect(passwordInput(page)).toHaveValue('Teacher-Remembered-1')
  239. await expect(page.locator('input[name="rememberMe"]')).toBeChecked()
  240. await page.locator('[data-role-code="admin"]').click()
  241. await expect(adminUsernameInput(page)).toHaveValue('admin.remembered')
  242. await expect(passwordInput(page)).toHaveValue('Admin-Remembered-2')
  243. await expect(page.locator('input[name="rememberMe"]')).toBeChecked()
  244. await page.locator('[data-role-code="teacher"]').click()
  245. await expect(identitySelect(page)).toContainText('杜晴')
  246. await expect(passwordInput(page)).toHaveValue('Teacher-Remembered-1')
  247. })
  248. test('成功登录后按勾选状态保存账号密码', async ({ page }) => {
  249. await page.route('**/api/auth/v1/auth/login', (route) => route.fulfill({
  250. status: 200,
  251. contentType: 'application/json',
  252. body: JSON.stringify({
  253. code: 200,
  254. message: '成功',
  255. data: { accessToken: 'remember-access-token', refreshToken: 'remember-refresh-token', expiresIn: 3600 },
  256. }),
  257. }))
  258. await page.route('**/api/auth/v1/auth/me', (route) => route.fulfill({
  259. status: 200,
  260. contentType: 'application/json',
  261. body: JSON.stringify({
  262. code: 200,
  263. message: '成功',
  264. data: {
  265. user: { id: '1', username: 'admin.saved', displayName: '系统管理员', mustChangePassword: true },
  266. activeRoleId: '1', roles: [{ id: '1', code: 'admin', name: '管理员', enabled: true }],
  267. permissions: [], authorizationMode: 'SINGLE_ACTIVE',
  268. },
  269. }),
  270. }))
  271. await openLoginWithContext(page)
  272. await adminUsernameInput(page).fill('admin.saved')
  273. await passwordInput(page).fill('Saved-Password-3')
  274. await page.locator('input[name="rememberMe"]').check()
  275. await submitButton(page).click()
  276. await expect.poll(() => page.evaluate(() => {
  277. const raw = window.localStorage.getItem('unreal-tran:web:login-identity-preference:v1') || '{}'
  278. return JSON.parse(raw).selections?.admin
  279. })).toEqual({
  280. departmentId: '', userId: '', username: 'admin.saved',
  281. password: 'Saved-Password-3', remember: true,
  282. })
  283. })
  284. test('已删除的部门或账号不会被本地偏好错误回填', async ({ page }) => {
  285. await page.addInitScript(() => {
  286. window.localStorage.setItem('unreal-tran:web:login-identity-preference:v1', JSON.stringify({
  287. lastRoleCode: 'teacher',
  288. selections: {
  289. teacher: { departmentId: '999', userId: '999', username: '' },
  290. },
  291. }))
  292. })
  293. await openLoginWithContext(page)
  294. await expect(page.locator('[data-role-code="teacher"]')).toHaveAttribute('aria-pressed', 'true')
  295. await expectElementSelectPlaceholder(page.getByTestId('login-organization'), '请选择教学系')
  296. await expect(page.getByTestId('login-department')).toBeDisabled()
  297. await expect(identitySelect(page)).toBeDisabled()
  298. await expect(passwordInput(page)).toHaveValue('')
  299. })
  300. test('默认管理员且四个身份展示各自约定字段', async ({ page }, testInfo) => {
  301. await openLoginWithContext(page)
  302. const roleButtons = page.locator('[data-role-code]')
  303. await expect(roleButtons).toHaveCount(4)
  304. expect(await roleButtons.evaluateAll((buttons) => buttons.map((button) => ({
  305. code: button.getAttribute('data-role-code'),
  306. label: button.textContent?.trim(),
  307. })))).toEqual([
  308. { code: 'administrative', label: '行政' },
  309. { code: 'teacher', label: '教员' },
  310. { code: 'student', label: '学员' },
  311. { code: 'admin', label: '管理员' },
  312. ])
  313. await expect(page.locator('[data-role-code="admin"]')).toHaveAttribute('aria-pressed', 'true')
  314. await expect(adminUsernameInput(page)).toHaveAttribute('placeholder', '请输入管理员账号')
  315. await expect(identitySelect(page)).toHaveCount(0)
  316. await expect(page.getByTestId('login-organization')).toHaveCount(0)
  317. await expect(page.getByTestId('login-department')).toHaveCount(0)
  318. await page.locator('[data-role-code="administrative"]').click()
  319. await expect(adminUsernameInput(page)).toHaveCount(0)
  320. await expect(identitySelect(page)).toBeVisible()
  321. await expectElementSelectPlaceholder(identitySelect(page), '请选择行政账号')
  322. await expect(identitySelect(page)).toBeDisabled()
  323. await expect(page.getByTestId('login-organization')).toHaveCount(0)
  324. await expect(page.getByTestId('login-department')).toBeVisible()
  325. await page.locator('[data-role-code="teacher"]').click()
  326. await expect(adminUsernameInput(page)).toHaveCount(0)
  327. await expect(identitySelect(page)).toBeVisible()
  328. await expectElementSelectPlaceholder(identitySelect(page), '请选择教员账号')
  329. await expect(identitySelect(page)).toBeDisabled()
  330. await expect(page.getByTestId('login-organization')).toBeVisible()
  331. await expect(page.getByTestId('login-department')).toBeVisible()
  332. await page.locator('[data-role-code="student"]').click()
  333. await expect(adminUsernameInput(page)).toHaveCount(0)
  334. await expect(identitySelect(page)).toBeVisible()
  335. await expectElementSelectPlaceholder(identitySelect(page), '请选择学员账号')
  336. await expect(identitySelect(page)).toBeEnabled()
  337. await expect(page.getByTestId('login-organization')).toHaveCount(0)
  338. await expect(page.getByTestId('login-department')).toHaveCount(0)
  339. await captureScreenshot(page, testInfo, 'four-role-fields')
  340. })
  341. test('行政身份按 SINGLE 模式只选择一个教学系', async ({ page }, testInfo) => {
  342. await openLoginWithContext(page)
  343. await page.locator('[data-role-code="administrative"]').click()
  344. const department = page.getByTestId('login-department')
  345. await expect(department).toBeEnabled()
  346. await expectElementSelectPlaceholder(department, '请选择教学系')
  347. await expectElementSelectOptions(page, department, [
  348. '工程装备系',
  349. '指挥系',
  350. ])
  351. await expect(submitButton(page)).toBeDisabled()
  352. await selectElementOption(page, department, '工程装备系')
  353. await expect(identitySelect(page)).toBeEnabled()
  354. await expectElementSelectPlaceholder(identitySelect(page), '请选择行政账号')
  355. await selectElementOption(page, identitySelect(page), /秦主任.*工程装备系/)
  356. await passwordInput(page).fill('ContractOnly-NotSubmitted')
  357. await expect(submitButton(page)).toBeEnabled()
  358. await captureScreenshot(page, testInfo, 'administrative-single')
  359. })
  360. test('教员身份按 CASCADE 模式支持教学系本级和直属教研室', async ({ page }, testInfo) => {
  361. await openLoginWithContext(page)
  362. await page.locator('[data-role-code="teacher"]').click()
  363. const organization = page.getByTestId('login-organization')
  364. const department = page.getByTestId('login-department')
  365. await expect(organization).toBeEnabled()
  366. await expect(department).toBeDisabled()
  367. await expect(identitySelect(page)).toBeDisabled()
  368. await expect(submitButton(page)).toBeDisabled()
  369. await selectElementOption(page, organization, '工程装备系')
  370. await expect(department).toBeEnabled()
  371. await expectElementSelectPlaceholder(department, '请选择教研室')
  372. await expectElementSelectOptions(page, department, [
  373. '工程装备系(本级)',
  374. '维修教研室',
  375. '智能保障教研室',
  376. ])
  377. await selectElementOption(page, department, '工程装备系(本级)')
  378. await expect(department).toContainText('工程装备系(本级)')
  379. await expect(identitySelect(page)).toBeEnabled()
  380. await selectElementOption(page, identitySelect(page), /许教员.*工程装备系/)
  381. await passwordInput(page).fill('ContractOnly-NotSubmitted')
  382. await expect(submitButton(page)).toBeEnabled()
  383. await selectElementOption(page, department, '维修教研室')
  384. await expect(department).toContainText('维修教研室')
  385. await expect(passwordInput(page)).toHaveValue('')
  386. await expect(identitySelect(page)).toBeEnabled()
  387. const identityListbox = await openElementSelect(page, identitySelect(page))
  388. await expect(identityListbox.getByRole('option', { name: /马雅柔.*水电组A/ })).toBeVisible()
  389. await identityListbox.getByRole('option', { name: /马雅柔.*水电组A/ }).click()
  390. await passwordInput(page).fill('ContractOnly-NotSubmitted')
  391. await expect(submitButton(page)).toBeEnabled()
  392. await captureScreenshot(page, testInfo, 'teacher-cascade-current-and-child')
  393. })
  394. test('切换身份会清空账号密码和已选组织', async ({ page }, testInfo) => {
  395. await openLoginWithContext(page)
  396. await page.locator('[data-role-code="teacher"]').click()
  397. await selectElementOption(page, page.getByTestId('login-organization'), '工程装备系')
  398. await selectElementOption(page, page.getByTestId('login-department'), '维修教研室')
  399. await selectElementOption(page, identitySelect(page), /马雅柔.*水电组A/)
  400. await passwordInput(page).fill('ContractOnly-NotSubmitted')
  401. await page.locator('[data-role-code="administrative"]').click()
  402. await expect(adminUsernameInput(page)).toHaveCount(0)
  403. await expectElementSelectPlaceholder(identitySelect(page), '请选择行政账号')
  404. await expect(passwordInput(page)).toHaveValue('')
  405. await expect(page.getByTestId('login-organization')).toHaveCount(0)
  406. await expectElementSelectPlaceholder(page.getByTestId('login-department'), '请选择教学系')
  407. await selectElementOption(page, page.getByTestId('login-department'), '指挥系')
  408. await selectElementOption(page, identitySelect(page), /周主任.*指挥系/)
  409. await passwordInput(page).fill('ContractOnly-NotSubmitted')
  410. await page.locator('[data-role-code="teacher"]').click()
  411. await expect(adminUsernameInput(page)).toHaveCount(0)
  412. await expectElementSelectPlaceholder(identitySelect(page), '请选择教员账号')
  413. await expect(passwordInput(page)).toHaveValue('')
  414. await expectElementSelectPlaceholder(page.getByTestId('login-organization'), '请选择教学系')
  415. await expectElementSelectPlaceholder(page.getByTestId('login-department'), '请选择教研室')
  416. await expect(page.getByTestId('login-department')).toBeDisabled()
  417. await captureScreenshot(page, testInfo, 'role-switch-clears-sensitive-fields')
  418. })
  419. test('登录页只按当前身份和组织加载安全账号候选且不会预填密码', async ({ page }) => {
  420. await openLoginWithContext(page)
  421. await expect(adminUsernameInput(page)).toHaveValue('')
  422. await expect(passwordInput(page)).toHaveValue('')
  423. await expect(adminUsernameInput(page)).toHaveAttribute('autocomplete', 'username')
  424. await expect(passwordInput(page)).toHaveAttribute('autocomplete', 'current-password')
  425. await expect(page.locator('.login-password-toggle')).toBeVisible()
  426. await expect(page.locator('.login-password-toggle svg')).toHaveCount(1)
  427. await expect(page.locator('.login-password-toggle')).toHaveAttribute('aria-label', '显示密码')
  428. await expect(page.locator('[data-password-visibility-icon="hidden"]')).toBeVisible()
  429. await page.locator('.login-password-toggle').click()
  430. await expect(passwordInput(page)).toHaveAttribute('type', 'text')
  431. await expect(page.locator('.login-password-toggle')).toHaveAttribute('aria-label', '隐藏密码')
  432. await expect(page.locator('[data-password-visibility-icon="visible"]')).toBeVisible()
  433. await page.locator('.login-password-toggle').click()
  434. await expect(passwordInput(page)).toHaveAttribute('type', 'password')
  435. await expect(page.locator('[data-password-visibility-icon="hidden"]')).toBeVisible()
  436. expect(await adminUsernameInput(page).getAttribute('list')).toBeNull()
  437. await expect(page.locator('datalist')).toHaveCount(0)
  438. await expect(page.locator('select[name*="user" i], select[name*="account" i]')).toHaveCount(0)
  439. await page.locator('[data-role-code="administrative"]').click()
  440. await expect(adminUsernameInput(page)).toHaveCount(0)
  441. await expect(identitySelect(page)).toBeDisabled()
  442. await expect(passwordInput(page)).toHaveValue('')
  443. await page.locator('[data-role-code="teacher"]').click()
  444. await expect(adminUsernameInput(page)).toHaveCount(0)
  445. await expect(identitySelect(page)).toBeDisabled()
  446. await expect(passwordInput(page)).toHaveValue('')
  447. await page.locator('[data-role-code="student"]').click()
  448. await expect(adminUsernameInput(page)).toHaveCount(0)
  449. await expect(identitySelect(page)).toBeEnabled()
  450. const studentListbox = await openElementSelect(page, identitySelect(page))
  451. await expect(studentListbox.getByRole('option', { name: /学生甲.*水电组A/ })).toBeVisible()
  452. await page.keyboard.press('Escape')
  453. await expect(passwordInput(page)).toHaveValue('')
  454. await page.locator('[data-role-code="admin"]').click()
  455. await expect(identitySelect(page)).toHaveCount(0)
  456. await expect(adminUsernameInput(page)).toHaveValue('')
  457. await expect(passwordInput(page)).toHaveValue('')
  458. })
  459. test('登录上下文失败时管理员账号密码入口仍可使用', async ({ page }, testInfo) => {
  460. await mockLoginContextFailure(page)
  461. await page.goto('/login')
  462. const contextState = page.getByText('认证配置连接失败', { exact: true })
  463. await expect(contextState).toBeAttached()
  464. await expect(contextState).toHaveAttribute('aria-live', 'polite')
  465. await expect(contextState).toBeVisible()
  466. await expect(page.locator('[data-role-code="admin"]')).toHaveAttribute('aria-pressed', 'true')
  467. await expect(page.getByTestId('login-organization')).toHaveCount(0)
  468. await expect(page.getByTestId('login-department')).toHaveCount(0)
  469. await adminUsernameInput(page).fill('admin.contract')
  470. await passwordInput(page).fill('ContractOnly-NotSubmitted')
  471. await expect(submitButton(page)).toBeEnabled()
  472. await captureScreenshot(page, testInfo, 'context-failure-admin-available')
  473. })
  474. })
  475. for (const viewport of [
  476. { width: 375, height: 812 },
  477. { width: 1024, height: 768 },
  478. { width: 1440, height: 900 },
  479. ]) {
  480. test(`登录页在 ${viewport.width}px 宽度无横向滚动`, async ({ page }, testInfo) => {
  481. await page.setViewportSize(viewport)
  482. await openLoginWithContext(page)
  483. const geometry = await page.evaluate(() => {
  484. const card = document.querySelector<HTMLElement>('.login-card')?.getBoundingClientRect()
  485. return {
  486. documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
  487. bodyOverflow: document.body.scrollWidth - document.body.clientWidth,
  488. cardLeft: card?.left ?? -1,
  489. cardRight: card?.right ?? Number.POSITIVE_INFINITY,
  490. viewportWidth: window.innerWidth,
  491. }
  492. })
  493. expect(geometry.documentOverflow).toBeLessThanOrEqual(1)
  494. expect(geometry.bodyOverflow).toBeLessThanOrEqual(1)
  495. expect(geometry.cardLeft).toBeGreaterThanOrEqual(0)
  496. expect(geometry.cardRight).toBeLessThanOrEqual(geometry.viewportWidth + 1)
  497. await captureScreenshot(page, testInfo, `login-responsive-${viewport.width}`)
  498. })
  499. }