Commit 8e4e7b372bbbb6ede896cf0ab5c7858218c9486f
1 parent
fbe53713
test(fe-login): 登录页 E2E 关键旅程 REQ-USR-004
Showing
3 changed files
with
127 additions
and
0 deletions
.gitignore
frontend/playwright.config.ts
0 → 100644
| 1 | +import { defineConfig, devices } from '@playwright/test'; | |
| 2 | + | |
| 3 | +// E2E:起 Vite dev server(端口 5173 = frontend.dev_port),用 page.route 桩后端,不依赖真实后端。 | |
| 4 | +const PORT = 5173; | |
| 5 | +const BASE_URL = `http://localhost:${PORT}`; | |
| 6 | + | |
| 7 | +export default defineConfig({ | |
| 8 | + testDir: './tests/e2e', | |
| 9 | + fullyParallel: true, | |
| 10 | + forbidOnly: !!process.env.CI, | |
| 11 | + retries: process.env.CI ? 2 : 0, | |
| 12 | + reporter: 'list', | |
| 13 | + use: { | |
| 14 | + baseURL: BASE_URL, | |
| 15 | + trace: 'on-first-retry', | |
| 16 | + }, | |
| 17 | + projects: [ | |
| 18 | + { | |
| 19 | + name: 'chromium', | |
| 20 | + use: { ...devices['Desktop Chrome'] }, | |
| 21 | + }, | |
| 22 | + ], | |
| 23 | + webServer: { | |
| 24 | + command: 'npm run dev', | |
| 25 | + url: BASE_URL, | |
| 26 | + reuseExistingServer: !process.env.CI, | |
| 27 | + timeout: 120_000, | |
| 28 | + }, | |
| 29 | +}); | ... | ... |
frontend/tests/e2e/login.spec.ts
0 → 100644
| 1 | +import { test, expect, type Page } from '@playwright/test'; | |
| 2 | + | |
| 3 | +// 桩后端:版本下拉与登录端点(page.route),不依赖真实后端起服。 | |
| 4 | +async function stubCompanies(page: Page) { | |
| 5 | + await page.route('**/api/usr/companies', async (route) => { | |
| 6 | + await route.fulfill({ | |
| 7 | + status: 200, | |
| 8 | + contentType: 'application/json', | |
| 9 | + body: JSON.stringify({ | |
| 10 | + code: 0, | |
| 11 | + message: 'success', | |
| 12 | + data: [ | |
| 13 | + { id: 1, sCompanyName: '甲公司', sVersion: '标准版' }, | |
| 14 | + { id: 2, sCompanyName: '乙公司', sVersion: null }, | |
| 15 | + ], | |
| 16 | + }), | |
| 17 | + }); | |
| 18 | + }); | |
| 19 | +} | |
| 20 | + | |
| 21 | +test.describe('登录页关键旅程', () => { | |
| 22 | + test('loads /login and shows version options', async ({ page }) => { | |
| 23 | + await stubCompanies(page); | |
| 24 | + await page.goto('/login'); | |
| 25 | + await expect(page.getByText('用户登录')).toBeVisible(); | |
| 26 | + // 打开版本下拉,应渲染桩返回项 | |
| 27 | + await page.getByRole('combobox').click(); | |
| 28 | + await expect(page.getByText('甲公司(标准版)')).toBeVisible(); | |
| 29 | + await expect(page.getByText('乙公司', { exact: true })).toBeVisible(); | |
| 30 | + }); | |
| 31 | + | |
| 32 | + test('blocks submit with validation when empty', async ({ page }) => { | |
| 33 | + await stubCompanies(page); | |
| 34 | + let loginCalled = false; | |
| 35 | + await page.route('**/api/usr/login', async (route) => { | |
| 36 | + loginCalled = true; | |
| 37 | + await route.fulfill({ | |
| 38 | + status: 200, | |
| 39 | + contentType: 'application/json', | |
| 40 | + body: JSON.stringify({ code: 0, message: 'success', data: {} }), | |
| 41 | + }); | |
| 42 | + }); | |
| 43 | + await page.goto('/login'); | |
| 44 | + await page.getByRole('button', { name: /登\s*录/ }).click(); | |
| 45 | + await expect(page.getByText('请输入用户名')).toBeVisible(); | |
| 46 | + await expect(page.getByText('请输入密码')).toBeVisible(); | |
| 47 | + expect(loginCalled).toBe(false); | |
| 48 | + }); | |
| 49 | + | |
| 50 | + test('successful login navigates away from /login', async ({ page }) => { | |
| 51 | + await stubCompanies(page); | |
| 52 | + await page.route('**/api/usr/login', async (route) => { | |
| 53 | + await route.fulfill({ | |
| 54 | + status: 200, | |
| 55 | + contentType: 'application/json', | |
| 56 | + body: JSON.stringify({ | |
| 57 | + code: 0, | |
| 58 | + message: 'success', | |
| 59 | + data: { | |
| 60 | + token: 'tk-e2e', | |
| 61 | + user: { id: 1, sUserName: 'admin', sUserType: '超级管理员', sLanguage: '中文' }, | |
| 62 | + }, | |
| 63 | + }), | |
| 64 | + }); | |
| 65 | + }); | |
| 66 | + await page.goto('/login'); | |
| 67 | + await page.getByPlaceholder('请输入你的用户名').fill('admin'); | |
| 68 | + await page.getByPlaceholder('请输入你的密码').fill('secret'); | |
| 69 | + // 选版本 | |
| 70 | + await page.getByRole('combobox').click(); | |
| 71 | + await page.getByText('甲公司(标准版)').click(); | |
| 72 | + await page.getByRole('button', { name: /登\s*录/ }).click(); | |
| 73 | + await expect(page.getByText('登录成功')).toBeVisible(); | |
| 74 | + await expect(page).not.toHaveURL(/\/login$/); | |
| 75 | + }); | |
| 76 | + | |
| 77 | + test('failed login stays on /login with error', async ({ page }) => { | |
| 78 | + await stubCompanies(page); | |
| 79 | + await page.route('**/api/usr/login', async (route) => { | |
| 80 | + await route.fulfill({ | |
| 81 | + status: 200, | |
| 82 | + contentType: 'application/json', | |
| 83 | + body: JSON.stringify({ code: 40101, message: '认证失败', data: null }), | |
| 84 | + }); | |
| 85 | + }); | |
| 86 | + await page.goto('/login'); | |
| 87 | + await page.getByPlaceholder('请输入你的用户名').fill('admin'); | |
| 88 | + await page.getByPlaceholder('请输入你的密码').fill('wrong'); | |
| 89 | + await page.getByRole('combobox').click(); | |
| 90 | + await page.getByText('甲公司(标准版)').click(); | |
| 91 | + await page.getByRole('button', { name: /登\s*录/ }).click(); | |
| 92 | + await expect(page.getByText('用户名或密码错误')).toBeVisible(); | |
| 93 | + await expect(page).toHaveURL(/\/login$/); | |
| 94 | + }); | |
| 95 | +}); | ... | ... |