From 8e4e7b372bbbb6ede896cf0ab5c7858218c9486f Mon Sep 17 00:00:00 2001 From: zichun Date: Mon, 1 Jun 2026 16:32:34 +0800 Subject: [PATCH] test(fe-login): 登录页 E2E 关键旅程 REQ-USR-004 --- .gitignore | 3 +++ frontend/playwright.config.ts | 29 +++++++++++++++++++++++++++++ frontend/tests/e2e/login.spec.ts | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 0 deletions(-) create mode 100644 frontend/playwright.config.ts create mode 100644 frontend/tests/e2e/login.spec.ts diff --git a/.gitignore b/.gitignore index d11b14b..97daf90 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ node_modules/ dist/ build/ coverage/ +playwright-report/ +test-results/ +.playwright/ # IDE .idea/ diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..a011e5f --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,29 @@ +import { defineConfig, devices } from '@playwright/test'; + +// E2E:起 Vite dev server(端口 5173 = frontend.dev_port),用 page.route 桩后端,不依赖真实后端。 +const PORT = 5173; +const BASE_URL = `http://localhost:${PORT}`; + +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: 'list', + use: { + baseURL: BASE_URL, + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'npm run dev', + url: BASE_URL, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, +}); diff --git a/frontend/tests/e2e/login.spec.ts b/frontend/tests/e2e/login.spec.ts new file mode 100644 index 0000000..35ed353 --- /dev/null +++ b/frontend/tests/e2e/login.spec.ts @@ -0,0 +1,95 @@ +import { test, expect, type Page } from '@playwright/test'; + +// 桩后端:版本下拉与登录端点(page.route),不依赖真实后端起服。 +async function stubCompanies(page: Page) { + await page.route('**/api/usr/companies', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + code: 0, + message: 'success', + data: [ + { id: 1, sCompanyName: '甲公司', sVersion: '标准版' }, + { id: 2, sCompanyName: '乙公司', sVersion: null }, + ], + }), + }); + }); +} + +test.describe('登录页关键旅程', () => { + test('loads /login and shows version options', async ({ page }) => { + await stubCompanies(page); + await page.goto('/login'); + await expect(page.getByText('用户登录')).toBeVisible(); + // 打开版本下拉,应渲染桩返回项 + await page.getByRole('combobox').click(); + await expect(page.getByText('甲公司(标准版)')).toBeVisible(); + await expect(page.getByText('乙公司', { exact: true })).toBeVisible(); + }); + + test('blocks submit with validation when empty', async ({ page }) => { + await stubCompanies(page); + let loginCalled = false; + await page.route('**/api/usr/login', async (route) => { + loginCalled = true; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ code: 0, message: 'success', data: {} }), + }); + }); + await page.goto('/login'); + await page.getByRole('button', { name: /登\s*录/ }).click(); + await expect(page.getByText('请输入用户名')).toBeVisible(); + await expect(page.getByText('请输入密码')).toBeVisible(); + expect(loginCalled).toBe(false); + }); + + test('successful login navigates away from /login', async ({ page }) => { + await stubCompanies(page); + await page.route('**/api/usr/login', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + code: 0, + message: 'success', + data: { + token: 'tk-e2e', + user: { id: 1, sUserName: 'admin', sUserType: '超级管理员', sLanguage: '中文' }, + }, + }), + }); + }); + await page.goto('/login'); + await page.getByPlaceholder('请输入你的用户名').fill('admin'); + await page.getByPlaceholder('请输入你的密码').fill('secret'); + // 选版本 + await page.getByRole('combobox').click(); + await page.getByText('甲公司(标准版)').click(); + await page.getByRole('button', { name: /登\s*录/ }).click(); + await expect(page.getByText('登录成功')).toBeVisible(); + await expect(page).not.toHaveURL(/\/login$/); + }); + + test('failed login stays on /login with error', async ({ page }) => { + await stubCompanies(page); + await page.route('**/api/usr/login', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ code: 40101, message: '认证失败', data: null }), + }); + }); + await page.goto('/login'); + await page.getByPlaceholder('请输入你的用户名').fill('admin'); + await page.getByPlaceholder('请输入你的密码').fill('wrong'); + await page.getByRole('combobox').click(); + await page.getByText('甲公司(标准版)').click(); + await page.getByRole('button', { name: /登\s*录/ }).click(); + await expect(page.getByText('用户名或密码错误')).toBeVisible(); + await expect(page).toHaveURL(/\/login$/); + }); +}); -- libgit2 0.22.2