// 登录/会话 e2e:真实后端(8080)+ admin dev(5176,/v1 代理)。 // 覆盖:路由守卫重定向、错误密码本地化报错、正确登录落到相册管理 stub、 // 令牌跨刷新持久(pinia persist → localStorage)、用户菜单登出(尽力吊销会话)。 // 注意:后端登录限速 15 分钟窗口 5 次失败——本文件只做一次错误密码尝试, // 短时间内反复全量重跑可能触发 AUTH_RATE_LIMITED(属预期行为,等待窗口过去即可)。 import { expect, test } from '@playwright/test' import { adminCredentials } from './helpers' test.beforeEach(async ({ page }) => { // 每个用例从干净会话开始(pinia persist 落在 localStorage) await page.goto('/') await page.evaluate(() => localStorage.clear()) }) test('未登录访问受保护路由 → 重定向登录页并携带 redirect', async ({ page }) => { await page.goto('/#/albums/manage') await expect(page.getByText('欢迎回来')).toBeVisible() await expect(page).toHaveURL(/\/auth\/login\?redirect=/) }) test('错误密码 → 本地化错误提示(AUTH_INVALID_CREDENTIALS)', async ({ page }) => { const { account } = adminCredentials() await page.goto('/#/auth/login') await page.getByPlaceholder('请输入账号').fill(account) await page.getByPlaceholder('请输入密码', { exact: false }).fill('wrong-password-e2e') await page.getByRole('button', { name: '登录' }).click() await expect(page.getByText('账号或密码错误')).toBeVisible() // 仍停留在登录页 await expect(page).toHaveURL(/\/auth\/login/) }) test('正确登录 → 落到相册管理占位页', async ({ page }) => { const { account, password } = adminCredentials() await page.goto('/#/auth/login') await page.getByPlaceholder('请输入账号').fill(account) await page.getByPlaceholder('请输入密码', { exact: false }).fill(password) await page.getByRole('button', { name: '登录' }).click() // 首页 '/' 重定向到菜单第一个有效路径(/albums/manage) await expect(page).toHaveURL(/\/albums\/manage/) await expect(page.getByText('相册管理').first()).toBeVisible() await expect(page.getByText('相册管理功能建设中').first()).toBeVisible() }) test('登录后刷新页面 → 会话令牌持久,受保护路由仍可达', async ({ page }) => { const { account, password } = adminCredentials() await page.goto('/#/auth/login') await page.getByPlaceholder('请输入账号').fill(account) await page.getByPlaceholder('请输入密码', { exact: false }).fill(password) await page.getByRole('button', { name: '登录' }).click() await expect(page).toHaveURL(/\/albums\/manage/) // 会话令牌已持久化(pinia-plugin-persistedstate → localStorage,键含版本前缀) const storage = await page.evaluate(() => JSON.stringify(localStorage)) expect(storage).toContain('sessionToken') // 刷新后不弹回登录页:动态路由重建,受保护页仍加载 await page.reload() await expect(page).toHaveURL(/\/albums\/manage/) await expect(page.getByText('相册管理功能建设中').first()).toBeVisible() }) test('用户菜单登出 → 尽力吊销会话并回到登录页', async ({ page }) => { const { account, password } = adminCredentials() await page.goto('/#/auth/login') await page.getByPlaceholder('请输入账号').fill(account) await page.getByPlaceholder('请输入密码', { exact: false }).fill(password) await page.getByRole('button', { name: '登录' }).click() await expect(page).toHaveURL(/\/albums\/manage/) // 头像 hover 弹出用户菜单 → 退出登录 → 确认 await page.getByAltText('avatar').hover() await page.getByText('退出登录').click() await page.getByRole('button', { name: '确定' }).click() // 用 heading 断言:页面还残留登录成功的 ElNotification(「欢迎回来, ...」文案) await expect(page.getByRole('heading', { name: '欢迎回来' })).toBeVisible({ timeout: 10_000 }) await expect(page).toHaveURL(/\/auth\/login/) // 本地会话已清空(user store 持久化键以 -user 结尾;字段保留但令牌为空串) const storedToken = await page.evaluate(() => { const entry = Object.entries(localStorage).find(([key]) => key.endsWith('-user')) return entry ? String(JSON.parse(entry[1]).sessionToken ?? '') : '__missing__' }) expect(storedToken).toBe('') })