K6 浏览器测试中处理重定向与页面加载完成的正确实践

在 k6 浏览器自动化测试中,`page.waitfornavigation()` 无法可靠捕获多步重定向后的最终页面状态,导致 `page.title()` 返回中间页标题;根本原因在于导航超时、上下文默认限制及缺乏对目标页面就绪状态的精准断言。

K6 的 k6/experimental/browser 模块基于 Playwright 内核,但其默认行为(如 waitForNavigation 的隐式超时、上下文级超时继承)与真实浏览器交互存在差异,尤其在涉及多跳重定向(如登录流程中的 /login → /auth → /dashboard)时极易出现“假完成”问题——即导航事件被过早触发,而 DOM 尚未渲染或关键资源未加载。

✅ 推荐解决方案:三重保障机制

1. 用 waitForSelector 替代 waitForNavigation(最可靠)

导航完成 ≠ 页面可用。应等待业务语义明确的元素(如仪表盘标题、用户头像、主内容区 ID)出现在 DOM 中:

// ✅ 正确做法:等待最终页核心元素
await Promise.all([
  page.waitForSelector('#dashboard-welcome-title', { timeout: 30000 }),
  continueButton2.click(),
]);
console.log(await page.title()); // 此时 title 必为最终页
⚠️ 注意:#dashboard-welcome-title 需替换为实际页面中仅在目标页存在且稳定渲染的选择器(避免 class 动态生成或 SSR/CSR 混合导致的竞态)。

2. 显式延长所有超时配置

waitForNavigation 默认超时约 30s,但 K6 上下文(Context)和页面(Page)可能继承更短的全局 timeout。需逐层覆盖:

const browser = chromium.launch({
  headless: false,
  timeout: '120s',     // Browser 实例超时
  slowMo: '500ms',
});

const context = browser.newContext({
  timeout: '120s',      // ✅ 关键:Context 级超时(影响 waitForNavigation 等)
});

const page = context.newPage();
// 同时为单次等待指定超时(防御性编程)
await page.waitForSelector('#final-page-element', { timeout: 45000 });

3. 禁用隐式 sleep(),改用条件化等待

sleep(10) 是反模式:既无法保证页面就绪(慢网/高负载下仍失败),又拖慢执行效率。所有等待必须基于可观测状态:

// ❌ 错误:固定休眠
sleep(10);

// ✅ 正确:条件化等待 + 超时兜底
await page.waitForFunction(
  () => document.querySelector('#app')?.dataset.ready === 'true',
  { timeout: 60000 }
);

? 完整修复示例(整合上述原则)

export async function browser() {
  const browser = chromium.launch({
    headless: false,
    timeout: '120s',
    slowMo: '500ms',
  });
  const context = browser.newContext({ timeout: '120s' }); // ← 关键补充
  const page = context.newPage();

  await page.goto('http://localhost:5000/', { waitUntil: 'networkidle' });

  // Cookie banner
  await page.locator('//*[@id="cookies-banner"]/div/div[2]/button').click();

  // Company input & continue
  await page.locator('#company-input').type('Test1');
  await Promise.all([
    page.waitForSelector('#login-method-selection-password-login-button', { timeout: 30000 }),
    page.locator('#company-login-continue-button').click(),
  ]);

  // Password login
  await Promise.all([
    page.waitForSelector('#user-name-input', { timeout: 30000 }), // 确保登录表单已加载
    page.locator('//*[@id="login-method-selection-password-login-button"]/p').click(),
  ]);

  // Final submit → wait for dashboard element
  await page.locator('#user-name-input').type('TestUsername');
  await page.locator('#password-input').type('TestPassword');

  await Promise.all([
    page.waitForSelector('#dashboard-main-content', { timeout: 45000 }), // ← 目标页唯一标识
    page.locator('//*[@id="password-login-continue-button"]/p').click(),
  ]);

  console.log('Final page title:', await page.title());
  console.log('URL:', page.url());

  await page.close();
  await browser.close();
}

? 总结要点

  • 永远优先等待具体 UI 元素,而非依赖 waitForNavigation(它只监听导航事件,不验证页面就绪);
  • Context 级 timeout 必须显式设置,否则 waitForNavigation 可能受默认 30s 限制;
  • 移除所有 sleep(),用 waitForSelector / waitForFunction 实现精准、可预测的等待;
  • 在 CI 环境中,建议添加 console.log(page.url()) 辅助调试重定向路径;
  • 若页面含动态 SPA 路由(如 React Router),需配合 waitForFunction 检查 window.location.pathname 或应用状态。

通过以上调整,可彻底解决重定向后 page.title() 返回错误标题的问题,并显著提升测试稳定性与可维护性。