Playwright成功获取Token但截图无法显示认证视图问题排查
问题描述
为采用JWT认证的React应用编写Playwright测试时,登录流程已成功获取access和refresh token,也能执行截图操作,但测试并未实际完成用户登录,截图无法显示认证后的视图,怀疑问题出在CurrentUserContext的使用上。
问题代码
测试文件frontend/playwright/signed-in-landing-page.spec.js中末尾的await page.screenshot({...});未能得到预期结果,完整代码如下:
import { test, expect } from '@playwright/test'; const BASE_URL = 'http://localhost:8000'; test.describe('Signed-in Landing Page', () => { test('desktop', async ({ page }) => { // Navigates to sign-in page await page.goto(`${BASE_URL}/signin`); // Fills in credentials and submit await page.fill('input[name="username"]', 'user'); await page.fill('input[name="password"]', 'qwerqwer*'); await page.click('button[type="submit"]'); // Waits for navigation and network idle await page.waitForLoadState('networkidle'); // Logs localStorage contents const localStorageData = await page.evaluate(() => { const data = {}; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); data[key] = localStorage.getItem(key); } return data; }); console.log('localStorage contents:', localStorageData); // Verifies user is signed in const accessToken = await page.evaluate(() => localStorage.getItem('accessToken')); console.log('Access token:', accessToken); if (accessToken) { // Check for an authenticated element const signOutElement = await page.locator('text=Sign out').count(); console.log('Sign out element count:', signOutElement); if (signOutElement === 0) { console.error('Sign out element not found'); const pageContent = await page.content(); console.log('Page content:', pageContent); } else { await expect(page.locator('text=Sign out')).toBeVisible(); } } // Take a screenshot. FAIL. The screenshot does not show Auth view as expected. await page.screenshot({ path: `screenshots/signed-in-landing-page-desktop.png`, fullPage: false }); }); });
已确认正常的基础测试
const { test, expect } = require('@playwright/test'); test('has title', async ({ page }) => { await page.goto('http://localhost:8080'); await expect(page).toHaveTitle(/Odyssey/); await page.waitForLoadState('networkidle'); // Takes a screenshot, returns the website in logged out view as expected await page.screenshot({ path: `screenshots/setup-confirmed.png`, fullPage: false }); });
Playwright配置文件
// @ts-check const { defineConfig, devices } = require('@playwright/test'); module.exports = defineConfig({ testDir: './playwright', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: [['list'], ['json', { outputFile: 'test-results.json' }]], outputDir: 'screenshots', use: { baseURL: 'http://localhost:8080', trace: 'on-first-retry', screenshot: 'on', headless: true, }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'], }, }, ], webServer: { command: 'python3 manage.py runserver', url: 'http://localhost:8080', reuseExistingServer: !process.env.CI, timeout: 120 * 1000 }, });
预期与实际行为
- 预期行为:成功获取Token后,截图显示登录后的视图
- 实际行为:测试成功获取Token并截图,但截图未显示登录后的视图
疑问
- 认证流程中是否遗漏关键步骤导致用户未完全登录?
- Token的存储或使用是否存在问题导致登录未完成?
环境与使用步骤
理想环境为Gitpod IDE:
- 终端1:运行
python3 manage.py runserver启动后端 - 终端2:进入
frontend目录,执行npm start启动前端 - 终端3:进入
frontend目录,执行npx install playwright --with-deps和xvfb-run npx playwright test [test-name here]运行测试
相关版本:Playwright 1.47.0、React 17.0.2、Node.js 16.0、Windows系统
排查思路与解决方案
1. 统一BASE_URL配置
测试文件中BASE_URL为http://localhost:8000,但Playwright配置和基础测试使用http://localhost:8080,端口不匹配会导致登录请求发送到错误地址,前端无法触发登录状态更新。修改测试文件中的BASE_URL为前端实际运行端口:
const BASE_URL = 'http://localhost:8080';
2. 等待登录后的路由跳转或关键元素
React单页应用登录后通常是路由跳转而非页面重载,仅用networkidle无法确保组件渲染完成。替换原等待逻辑,改为等待目标路由或认证元素:
// 替换原waitForLoadState await page.waitForURL(`${BASE_URL}/dashboard`); // 替换为实际登录后跳转的路由 await expect(page.locator('text=Sign out')).toBeVisible({ timeout: 5000 });
3. 验证CurrentUserContext状态同步
React应用中登录后需等待Context状态更新到组件,可通过等待登录后独有的元素(如用户名、用户头像)确认状态已同步:
await expect(page.locator('[data-testid="user-avatar"]')).toBeVisible();
4. 检查Token有效性
获取到Token不代表其有效,可在测试中解码JWT验证过期时间和用户信息:
const accessToken = await page.evaluate(() => localStorage.getItem('accessToken')); if (accessToken) { const payload = JSON.parse(atob(accessToken.split('.')[1])); expect(payload.exp * 1000).toBeGreaterThan(Date.now()); // 验证token未过期 }
5. 开启可视化调试
将Playwright配置中的headless: true改为false,并添加慢动作执行,直观观察测试过程:
use: { // ...其他配置 headless: false, slowMo: 500, }
6. 监听登录请求响应
确认后端返回的登录响应是否正常,避免因接口错误导致前端未更新状态:
// 登录前监听登录接口 const loginRequest = page.waitForRequest(req => req.url().includes('/api/auth/login') && req.method() === 'POST'); await page.click('button[type="submit"]'); const response = await loginRequest; expect(response.status()).toBe(200); console.log('Login response:', await response.json());
内容的提问来源于stack exchange,提问作者Laurie Crean

