user-journeys

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

User Journeys Skill

用户旅程技能

Load with: base.md + playwright-testing.md
For defining and testing real user experiences - not just specs, but actual flows humans take through your application.

加载依赖:base.md + playwright-testing.md
用于定义和测试真实用户体验——不仅仅是规格说明,而是用户在应用中实际经历的流程。

Philosophy

设计理念

Specs test features. Journeys test experiences.
A feature can pass all specs but still deliver a terrible experience. User journeys capture:
  • How users actually navigate (not how we think they should)
  • Emotional states at each step (frustrated, confused, delighted)
  • Recovery from mistakes (users will make them)
  • Real-world conditions (slow networks, interruptions, distractions)

规格说明测试功能,旅程测试体验。
一个功能可能通过所有规格测试,但仍可能带来糟糕的体验。用户旅程涵盖:
  • 用户实际的导航方式(而非我们设想的方式)
  • 每个步骤的用户情绪状态(沮丧、困惑、愉悦)
  • 错误后的恢复流程(用户必然会犯错)
  • 真实场景条件(网络缓慢、中断、干扰)

Journey Documentation Structure

旅程文档结构

_project_specs/
├── journeys/
│   ├── _template.md              # Journey template
│   ├── critical/                 # Must-work journeys (revenue, core value)
│   │   ├── signup-to-first-value.md
│   │   ├── checkout-purchase.md
│   │   └── login-to-dashboard.md
│   ├── common/                   # Frequent user paths
│   │   ├── browse-and-search.md
│   │   ├── update-profile.md
│   │   └── invite-team-member.md
│   └── edge-cases/               # Error recovery, unusual paths
│       ├── payment-failure-retry.md
│       ├── session-timeout-recovery.md
│       └── offline-reconnection.md

_project_specs/
├── journeys/
│   ├── _template.md              # 旅程模板
│   ├── critical/                 # 核心必测旅程(营收、核心价值相关)
│   │   ├── signup-to-first-value.md
│   │   ├── checkout-purchase.md
│   │   └── login-to-dashboard.md
│   ├── common/                   # 高频用户路径
│   │   ├── browse-and-search.md
│   │   ├── update-profile.md
│   │   └── invite-team-member.md
│   └── edge-cases/               # 错误恢复、异常路径
│       ├── payment-failure-retry.md
│       ├── session-timeout-recovery.md
│       └── offline-reconnection.md

Journey Template

旅程模板

markdown
undefined
markdown
undefined

Journey: [Name]

旅程:[名称]

Overview

概览

AttributeValue
PriorityCritical / High / Medium
User TypeNew / Returning / Admin
FrequencyDaily / Weekly / One-time
Success MetricConversion rate, time to complete, drop-off rate
属性
优先级核心 / 高 / 中
用户类型新用户 / 回头客 / 管理员
发生频率每日 / 每周 / 一次性
成功指标转化率、完成时长、流失率

User Goal

用户目标

What is the user trying to accomplish? Write from their perspective.
"I want to [goal] so that I can [benefit]."
用户想要达成什么?从用户视角描述。
"我想要[达成目标],这样我就能[获得收益]。"

Preconditions

前置条件

  • User state (logged in, has subscription, first visit)
  • Data state (has items in cart, has team members)
  • Environment (mobile, desktop, slow connection)
  • 用户状态(已登录、已订阅、首次访问)
  • 数据状态(购物车有商品、已有团队成员)
  • 环境(移动端、桌面端、网络缓慢)

Journey Steps

旅程步骤

Step 1: [Entry Point]

步骤1:[入口点]

User Action: What the user does System Response: What they should see/experience Success Criteria:
  • Page loads in < 2 seconds
  • Primary CTA is immediately visible
  • User understands what to do next
Potential Friction:
  • Slow load time → Show skeleton/loader
  • Unclear CTA → A/B test copy variations

用户操作: 用户执行的动作 系统响应: 用户应看到/体验到的内容 成功标准:
  • 页面加载时长 < 2秒
  • 主要CTA(号召性用语)立即可见
  • 用户清楚下一步操作
潜在阻碍:
  • 加载缓慢 → 显示骨架屏/加载动画
  • CTA不清晰 → A/B测试不同文案

Step 2: [Next Action]

步骤2:[下一步操作]

User Action: ... System Response: ... Success Criteria:
  • ...
Potential Friction:
  • ...

用户操作: ... 系统响应: ... 成功标准:
  • ...
潜在阻碍:
  • ...

Error Scenarios

错误场景

E1: [Error Name]

E1:[错误名称]

Trigger: What causes this error User Sees: Error message/state Recovery Path: How user gets back on track Test: How to verify recovery works
触发条件: 导致错误的原因 用户看到: 错误提示/状态 恢复路径: 用户如何回到正轨 测试方法: 验证恢复流程的有效性

Metrics to Track

跟踪指标

  • Time to complete journey
  • Drop-off rate at each step
  • Error rate and recovery rate
  • User satisfaction (if surveyed)
  • 旅程完成时长
  • 各步骤流失率
  • 错误率与恢复率
  • 用户满意度(若有调研)

E2E Test Reference

端到端测试引用

Link to Playwright test:
e2e/tests/journeys/[name].spec.ts

---
关联Playwright测试:
e2e/tests/journeys/[name].spec.ts

---

Critical Journey Examples

核心旅程示例

Signup to First Value

注册至首次价值体验

markdown
undefined
markdown
undefined

Journey: Signup to First Value

旅程:注册至首次价值体验

Overview

概览

AttributeValue
PriorityCritical
User TypeNew
FrequencyOne-time
Success Metric% reaching "aha moment" within 5 min
属性
优先级核心
用户类型新用户
发生频率一次性
成功指标5分钟内达到"惊喜时刻"的用户占比

User Goal

用户目标

"I want to try this product quickly to see if it solves my problem."
"我想快速试用这款产品,看看它能否解决我的问题。"

Preconditions

前置条件

  • First visit to site
  • No account
  • Came from landing page or ad
  • 首次访问网站
  • 无账号
  • 来自落地页或广告

Journey Steps

旅程步骤

Step 1: Landing Page

步骤1:落地页

User Action: Clicks "Get Started Free" or "Try Now" System Response: Signup form appears (modal or new page) Success Criteria:
  • CTA visible above fold
  • No distracting elements
  • Clear value proposition visible
Potential Friction:
  • Too many form fields → Reduce to email + password only
  • Social login missing → Add Google/GitHub options
用户操作: 点击"免费开始"或"立即试用" 系统响应: 弹出注册表单(模态框或新页面) 成功标准:
  • CTA在首屏可见
  • 无干扰元素
  • 清晰的价值主张可见
潜在阻碍:
  • 表单字段过多 → 仅保留邮箱+密码
  • 缺少社交登录 → 添加Google/GitHub登录选项

Step 2: Account Creation

步骤2:账号创建

User Action: Enters email and password (or uses social login) System Response:
  • Creates account
  • Sends verification email (don't block on it)
  • Redirects to onboarding
Success Criteria:
  • Account created in < 3 seconds
  • No email verification wall (verify later)
  • Clear next step shown
Potential Friction:
  • Email already exists → Offer login link
  • Weak password → Show requirements inline, not after submit
用户操作: 输入邮箱和密码(或使用社交登录) 系统响应:
  • 创建账号
  • 发送验证邮件(不强制等待验证)
  • 重定向至引导页
成功标准:
  • 账号创建时长 < 3秒
  • 无邮箱验证墙(可后续验证)
  • 清晰展示下一步操作
潜在阻碍:
  • 邮箱已注册 → 提供登录链接
  • 密码强度不足 → 实时显示要求,而非提交后提示

Step 3: Onboarding (Quick Win)

步骤3:引导流程(快速见效)

User Action: Completes 1-2 setup questions System Response:
  • Personalizes experience
  • Shows progress indicator
  • Leads to first action
Success Criteria:
  • Max 3 questions
  • Skip option available
  • < 60 seconds total
Potential Friction:
  • Too many questions → User abandons
  • No skip option → User feels trapped
用户操作: 完成1-2个设置问题 系统响应:
  • 个性化体验
  • 显示进度指示器
  • 引导至首次核心操作
成功标准:
  • 最多3个问题
  • 提供跳过选项
  • 总时长 < 60秒
潜在阻碍:
  • 问题过多 → 用户放弃
  • 无跳过选项 → 用户感到受限

Step 4: First Value (Aha Moment)

步骤4:首次价值体验(惊喜时刻)

User Action: Completes core action (creates first X, sees first result) System Response:
  • Celebrates success
  • Shows value delivered
  • Suggests next step
Success Criteria:
  • User experiences core value
  • Completion feels rewarding
  • Clear path to continue
用户操作: 完成核心操作(创建首个内容、查看首个结果) 系统响应:
  • 庆祝成功
  • 展示已交付的价值
  • 建议下一步操作
成功标准:
  • 用户体验到核心价值
  • 完成操作有成就感
  • 清晰的后续路径

Error Scenarios

错误场景

E1: Email Already Registered

E1:邮箱已注册

Trigger: User tries existing email User Sees: "Already have an account? Log in or reset password" Recovery Path: Click to login or reset Test:
signup-existing-email.spec.ts
触发条件: 用户尝试使用已注册的邮箱 用户看到: "已有账号?登录或重置密码" 恢复路径: 点击登录或重置密码 测试:
signup-existing-email.spec.ts

E2: Social Login Fails

E2:社交登录失败

Trigger: OAuth provider error User Sees: "Couldn't connect. Try email signup or try again." Recovery Path: Email signup form shown as fallback Test:
social-login-failure.spec.ts
触发条件: OAuth提供商错误 用户看到: "连接失败。请尝试邮箱注册或重试。" 恢复路径: 显示邮箱注册表单作为备选 测试:
social-login-failure.spec.ts

Metrics to Track

跟踪指标

  • Signup → First Value: Target < 5 min
  • Drop-off at each step
  • Social vs email signup ratio
  • Skip rate on onboarding

---
  • 注册→首次价值体验:目标时长 < 5分钟
  • 各步骤流失率
  • 社交登录与邮箱登录占比
  • 引导流程跳过率

---

Checkout Purchase

结账购买

markdown
undefined
markdown
undefined

Journey: Checkout Purchase

旅程:结账购买

Overview

概览

AttributeValue
PriorityCritical (Revenue)
User TypeAny
FrequencyVariable
Success MetricCheckout completion rate
属性
优先级核心(营收相关)
用户类型所有用户
发生频率不定
成功指标结账完成率

User Goal

用户目标

"I want to pay quickly and securely without surprises."
"我想快速、安全地完成支付,没有意外费用。"

Journey Steps

旅程步骤

Step 1: Cart Review

步骤1:购物车审核

User Action: Views cart before checkout System Response:
  • Shows all items with images, prices
  • Shows subtotal, taxes, shipping
  • Clear "Checkout" CTA
Success Criteria:
  • No hidden fees revealed later
  • Easy to modify quantities
  • Saved items visible
用户操作: 结账前查看购物车 系统响应:
  • 显示所有商品的图片、价格
  • 显示小计、税费、运费
  • 清晰的"结账"CTA
成功标准:
  • 无后续隐藏费用
  • 可轻松修改商品数量
  • 已保存的商品可见

Step 2: Checkout Start

步骤2:开始结账

User Action: Clicks "Checkout" System Response:
  • Shows checkout form or redirect to payment
  • Progress indicator (Step 1 of 3)
  • Order summary sidebar
Success Criteria:
  • Guest checkout option
  • Express checkout (Apple/Google Pay) prominent
  • Form fields pre-filled if logged in
用户操作: 点击"结账" 系统响应:
  • 显示结账表单或重定向至支付页面
  • 进度指示器(第1步,共3步)
  • 侧边栏显示订单摘要
成功标准:
  • 支持访客结账
  • 快捷支付(Apple/Google Pay)突出显示
  • 已登录用户自动填充表单字段

Step 3: Payment

步骤3:支付

User Action: Enters payment info System Response:
  • Secure input fields (Stripe/payment provider)
  • Real-time validation
  • Clear "Pay $XX" button
Success Criteria:
  • Card validation inline, not after submit
  • Multiple payment options
  • Security indicators visible
用户操作: 输入支付信息 系统响应:
  • 安全输入字段(Stripe/支付提供商)
  • 实时验证
  • 清晰的"支付$XX"按钮
成功标准:
  • 卡片信息实时验证,而非提交后
  • 多种支付选项
  • 安全标识可见

Step 4: Confirmation

步骤4:确认

User Action: Submits payment System Response:
  • Processing indicator
  • Success page with order details
  • Email confirmation sent
Success Criteria:
  • Confirmation within 5 seconds
  • Order number clearly visible
  • Next steps clear (shipping, access, etc.)
用户操作: 提交支付 系统响应:
  • 显示处理中指示器
  • 成功页面显示订单详情
  • 发送确认邮件
成功标准:
  • 确认页面加载时长 < 5秒
  • 订单号清晰可见
  • 后续步骤明确(发货、访问权限等)

Error Scenarios

错误场景

E1: Payment Declined

E1:支付被拒

Trigger: Card declined by processor User Sees: "Payment declined. Please try another card." Recovery Path:
  • Stay on payment step
  • Pre-fill other fields
  • Offer alternative payment methods Test:
    payment-declined-recovery.spec.ts
触发条件: 支付处理器拒绝卡片 用户看到: "支付被拒。请尝试其他卡片。" 恢复路径:
  • 停留在支付步骤
  • 自动填充其他字段
  • 提供备选支付方式 测试:
    payment-declined-recovery.spec.ts

E2: Session Timeout During Checkout

E2:结账时会话超时

Trigger: User away too long User Sees: Cart preserved, re-auth required Recovery Path:
  • Quick login
  • Return to same checkout step
  • Cart contents intact Test:
    checkout-session-timeout.spec.ts

---
触发条件: 用户离开时间过长 用户看到: 购物车保留,需重新验证 恢复路径:
  • 快速登录
  • 返回至同一结账步骤
  • 购物车内容完整 测试:
    checkout-session-timeout.spec.ts

---

Journey Testing with Playwright

使用Playwright进行旅程测试

Journey Test Structure

旅程测试结构

typescript
// e2e/tests/journeys/signup-to-value.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Journey: Signup to First Value', () => {
  test.describe.configure({ mode: 'serial' }); // Run in order

  test('Step 1: Landing page has clear CTA', async ({ page }) => {
    await page.goto('/');

    // CTA visible above fold without scrolling
    const cta = page.getByRole('button', { name: /get started|try free/i });
    await expect(cta).toBeVisible();
    await expect(cta).toBeInViewport();
  });

  test('Step 2: Can create account quickly', async ({ page }) => {
    await page.goto('/');
    await page.getByRole('button', { name: /get started/i }).click();

    // Minimal fields
    await expect(page.getByLabel('Email')).toBeVisible();
    await expect(page.getByLabel('Password')).toBeVisible();

    // Complete signup
    const startTime = Date.now();
    await page.getByLabel('Email').fill('newuser@example.com');
    await page.getByLabel('Password').fill('SecurePass123!');
    await page.getByRole('button', { name: /sign up|create/i }).click();

    // Should reach onboarding quickly
    await expect(page).toHaveURL(/onboarding|welcome|setup/);
    expect(Date.now() - startTime).toBeLessThan(5000); // < 5 seconds
  });

  test('Step 3: Onboarding is skippable', async ({ page }) => {
    // ... login as new user ...
    await page.goto('/onboarding');

    // Skip option exists
    const skipButton = page.getByRole('button', { name: /skip/i });
    await expect(skipButton).toBeVisible();
  });

  test('Step 4: Can reach first value in < 5 min', async ({ page }) => {
    // Full journey timing
    const journeyStart = Date.now();

    // ... complete full journey ...

    // Verify first value delivered
    await expect(page.getByText(/success|created|done/i)).toBeVisible();

    // Total time check
    const totalTime = (Date.now() - journeyStart) / 1000 / 60; // minutes
    expect(totalTime).toBeLessThan(5);
  });
});
typescript
// e2e/tests/journeys/signup-to-value.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Journey: Signup to First Value', () => {
  test.describe.configure({ mode: 'serial' }); // 按顺序执行

  test('Step 1: Landing page has clear CTA', async ({ page }) => {
    await page.goto('/');

    // CTA在首屏可见,无需滚动
    const cta = page.getByRole('button', { name: /get started|try free/i });
    await expect(cta).toBeVisible();
    await expect(cta).toBeInViewport();
  });

  test('Step 2: Can create account quickly', async ({ page }) => {
    await page.goto('/');
    await page.getByRole('button', { name: /get started/i }).click();

    // 最少字段
    await expect(page.getByLabel('Email')).toBeVisible();
    await expect(page.getByLabel('Password')).toBeVisible();

    // 完成注册
    const startTime = Date.now();
    await page.getByLabel('Email').fill('newuser@example.com');
    await page.getByLabel('Password').fill('SecurePass123!');
    await page.getByRole('button', { name: /sign up|create/i }).click();

    // 应快速进入引导页
    await expect(page).toHaveURL(/onboarding|welcome|setup/);
    expect(Date.now() - startTime).toBeLessThan(5000); // < 5秒
  });

  test('Step 3: Onboarding is skippable', async ({ page }) => {
    // ... 以新用户身份登录 ...
    await page.goto('/onboarding');

    // 存在跳过选项
    const skipButton = page.getByRole('button', { name: /skip/i });
    await expect(skipButton).toBeVisible();
  });

  test('Step 4: Can reach first value in < 5 min', async ({ page }) => {
    // 完整旅程计时
    const journeyStart = Date.now();

    // ... 完成完整旅程 ...

    // 验证首次价值已交付
    await expect(page.getByText(/success|created|done/i)).toBeVisible();

    // 总时长检查
    const totalTime = (Date.now() - journeyStart) / 1000 / 60; // 分钟
    expect(totalTime).toBeLessThan(5);
  });
});

Error Recovery Tests

错误恢复测试

typescript
// e2e/tests/journeys/checkout-recovery.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Journey: Checkout Error Recovery', () => {
  test('recovers from payment decline gracefully', async ({ page }) => {
    // Setup: Add item to cart, go to checkout
    await page.goto('/products');
    await page.getByTestId('add-to-cart').first().click();
    await page.getByRole('link', { name: 'Checkout' }).click();

    // Use Stripe test card that declines
    const stripeFrame = page.frameLocator('iframe[name*="stripe"]');
    await stripeFrame.getByPlaceholder('Card number').fill('4000000000000002');
    await stripeFrame.getByPlaceholder('MM / YY').fill('12/30');
    await stripeFrame.getByPlaceholder('CVC').fill('123');

    await page.getByRole('button', { name: /pay/i }).click();

    // Verify friendly error
    await expect(page.getByText(/declined|try another/i)).toBeVisible();

    // Verify still on checkout (not kicked out)
    await expect(page).toHaveURL(/checkout/);

    // Verify can try again with different card
    await stripeFrame.getByPlaceholder('Card number').fill('4242424242424242');
    await page.getByRole('button', { name: /pay/i }).click();

    // Should succeed now
    await expect(page).toHaveURL(/success|confirmation/);
  });

  test('preserves cart after session timeout', async ({ page, context }) => {
    // Add items to cart
    await page.goto('/products');
    await page.getByTestId('add-to-cart').first().click();

    // Clear session (simulate timeout)
    await context.clearCookies();

    // Return to site
    await page.goto('/cart');

    // Cart should be preserved (local storage or recovered)
    await expect(page.getByTestId('cart-item')).toHaveCount(1);
  });
});

typescript
// e2e/tests/journeys/checkout-recovery.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Journey: Checkout Error Recovery', () => {
  test('recovers from payment decline gracefully', async ({ page }) => {
    // 准备:添加商品至购物车,进入结账页
    await page.goto('/products');
    await page.getByTestId('add-to-cart').first().click();
    await page.getByRole('link', { name: 'Checkout' }).click();

    // 使用Stripe测试用的拒付卡片
    const stripeFrame = page.frameLocator('iframe[name*="stripe"]');
    await stripeFrame.getByPlaceholder('Card number').fill('4000000000000002');
    await stripeFrame.getByPlaceholder('MM / YY').fill('12/30');
    await stripeFrame.getByPlaceholder('CVC').fill('123');

    await page.getByRole('button', { name: /pay/i }).click();

    // 验证友好的错误提示
    await expect(page.getByText(/declined|try another/i)).toBeVisible();

    // 验证仍停留在结账页(未被退出)
    await expect(page).toHaveURL(/checkout/);

    // 验证可更换卡片重试
    await stripeFrame.getByPlaceholder('Card number').fill('4242424242424242');
    await page.getByRole('button', { name: /pay/i }).click();

    // 此次应支付成功
    await expect(page).toHaveURL(/success|confirmation/);
  });

  test('preserves cart after session timeout', async ({ page, context }) => {
    // 添加商品至购物车
    await page.goto('/products');
    await page.getByTestId('add-to-cart').first().click();

    // 清除会话(模拟超时)
    await context.clearCookies();

    // 返回网站
    await page.goto('/cart');

    // 购物车应被保留(本地存储或恢复)
    await expect(page.getByTestId('cart-item')).toHaveCount(1);
  });
});

User Experience Validation

用户体验验证

UX Checklist per Journey Step

各旅程步骤UX检查清单

markdown
undefined
markdown
undefined

UX Validation Checklist

UX验证检查清单

Clarity

清晰度

  • User knows where they are (breadcrumbs, progress)
  • User knows what to do next (clear CTA)
  • User knows what just happened (feedback)
  • 用户清楚当前位置(面包屑、进度条)
  • 用户清楚下一步操作(清晰CTA)
  • 用户清楚刚发生的操作(反馈)

Speed

速度

  • Page loads < 2 seconds
  • Actions complete < 3 seconds
  • Progress shown for longer operations
  • 页面加载时长 < 2秒
  • 操作完成时长 < 3秒
  • 长时操作显示进度

Forgiveness

容错性

  • Mistakes are easy to undo
  • Errors explain what went wrong
  • Recovery path is clear
  • 错误易于撤销
  • 错误提示说明问题原因
  • 恢复路径清晰

Accessibility

可访问性

  • Keyboard navigation works
  • Screen reader announces changes
  • Focus management correct
  • Color contrast sufficient
  • 支持键盘导航
  • 屏幕阅读器播报变更
  • 焦点管理正确
  • 颜色对比度达标

Mobile

移动端适配

  • Touch targets >= 44px
  • No horizontal scroll
  • Forms don't zoom unexpectedly
  • Works on slow 3G
undefined
  • 触摸目标 >= 44px
  • 无横向滚动
  • 表单不会意外缩放
  • 在3G慢网络下正常工作
undefined

Automated UX Checks

自动化UX检查

typescript
// e2e/utils/ux-validators.ts
import { Page, expect } from '@playwright/test';

export async function validatePageLoad(page: Page, maxMs = 2000) {
  const timing = await page.evaluate(() => {
    const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
    return nav.loadEventEnd - nav.startTime;
  });
  expect(timing).toBeLessThan(maxMs);
}

export async function validateCTAVisible(page: Page, ctaText: RegExp) {
  const cta = page.getByRole('button', { name: ctaText });
  await expect(cta).toBeVisible();
  await expect(cta).toBeInViewport();
}

export async function validateNoLayoutShift(page: Page) {
  const cls = await page.evaluate(() => {
    return new Promise<number>((resolve) => {
      let clsValue = 0;
      const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (!(entry as any).hadRecentInput) {
            clsValue += (entry as any).value;
          }
        }
      });
      observer.observe({ type: 'layout-shift', buffered: true });
      setTimeout(() => {
        observer.disconnect();
        resolve(clsValue);
      }, 1000);
    });
  });
  expect(cls).toBeLessThan(0.1); // Good CLS score
}

export async function validateAccessibility(page: Page) {
  // Check focus visible on interactive elements
  const buttons = page.getByRole('button');
  const count = await buttons.count();

  for (let i = 0; i < Math.min(count, 5); i++) {
    await buttons.nth(i).focus();
    await expect(buttons.nth(i)).toBeFocused();
  }
}

typescript
// e2e/utils/ux-validators.ts
import { Page, expect } from '@playwright/test';

export async function validatePageLoad(page: Page, maxMs = 2000) {
  const timing = await page.evaluate(() => {
    const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
    return nav.loadEventEnd - nav.startTime;
  });
  expect(timing).toBeLessThan(maxMs);
}

export async function validateCTAVisible(page: Page, ctaText: RegExp) {
  const cta = page.getByRole('button', { name: ctaText });
  await expect(cta).toBeVisible();
  await expect(cta).toBeInViewport();
}

export async function validateNoLayoutShift(page: Page) {
  const cls = await page.evaluate(() => {
    return new Promise<number>((resolve) => {
      let clsValue = 0;
      const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (!(entry as any).hadRecentInput) {
            clsValue += (entry as any).value;
          }
        }
      });
      observer.observe({ type: 'layout-shift', buffered: true });
      setTimeout(() => {
        observer.disconnect();
        resolve(clsValue);
      }, 1000);
    });
  });
  expect(cls).toBeLessThan(0.1); // 良好的CLS分数
}

export async function validateAccessibility(page: Page) {
  // 检查交互元素的焦点可见性
  const buttons = page.getByRole('button');
  const count = await buttons.count();

  for (let i = 0; i < Math.min(count, 5); i++) {
    await buttons.nth(i).focus();
    await expect(buttons.nth(i)).toBeFocused();
  }
}

Journey Metrics Dashboard

旅程指标仪表盘

Track journey health with these metrics:
typescript
// lib/journey-metrics.ts
interface JourneyMetric {
  journey: string;
  step: string;
  timestamp: Date;
  duration: number;
  success: boolean;
  userId?: string;
}

// Track in your analytics (PostHog, Mixpanel, etc.)
export function trackJourneyStep(metric: JourneyMetric) {
  analytics.track('journey_step', {
    journey_name: metric.journey,
    step_name: metric.step,
    duration_ms: metric.duration,
    success: metric.success,
  });
}

// Example usage in app
const journeyStart = Date.now();
// ... user completes step ...
trackJourneyStep({
  journey: 'signup_to_value',
  step: 'account_creation',
  timestamp: new Date(),
  duration: Date.now() - journeyStart,
  success: true,
});

通过以下指标跟踪旅程健康状况:
typescript
// lib/journey-metrics.ts
interface JourneyMetric {
  journey: string;
  step: string;
  timestamp: Date;
  duration: number;
  success: boolean;
  userId?: string;
}

// 在分析工具中跟踪(PostHog、Mixpanel等)
export function trackJourneyStep(metric: JourneyMetric) {
  analytics.track('journey_step', {
    journey_name: metric.journey,
    step_name: metric.step,
    duration_ms: metric.duration,
    success: metric.success,
  });
}

// 应用中的示例用法
const journeyStart = Date.now();
// ... 用户完成步骤 ...
trackJourneyStep({
  journey: 'signup_to_value',
  step: 'account_creation',
  timestamp: new Date(),
  duration: Date.now() - journeyStart,
  success: true,
});

Common Journey Patterns

常见旅程模式

Progressive Disclosure Journey

渐进式披露旅程

User sees simple view first, complexity revealed as needed.
markdown
Step 1: Show basic options only
Step 2: "Advanced" expands more options
Step 3: Expert mode unlocks everything
用户先看到简单视图,按需展示复杂内容。
markdown
步骤1:仅显示基础选项
步骤2:"高级"选项展开更多功能
步骤3:专家模式解锁全部功能

Guided Setup Journey

引导式设置旅程

Hand-hold new users through initial configuration.
markdown
Step 1: Welcome + single choice
Step 2: Core preference
Step 3: Optional integrations (skippable)
Step 4: First action with guidance
Step 5: Success + remove training wheels
全程协助新用户完成初始配置。
markdown
步骤1:欢迎页 + 单项选择
步骤2:核心偏好设置
步骤3:可选集成(可跳过)
步骤4:带引导的首次操作
步骤5:成功提示 + 移除引导

Recovery Journey

恢复旅程

User returns after failure or abandonment.
markdown
Step 1: Recognize returning user
Step 2: Restore previous state
Step 3: Acknowledge what happened
Step 4: Offer clear path forward
Step 5: Complete original goal

用户在失败或放弃后返回。
markdown
步骤1:识别回头用户
步骤2:恢复之前的状态
步骤3:告知用户之前的情况
步骤4:提供清晰的前进路径
步骤5:完成原目标

Anti-Patterns

反模式

  • Happy path only - Test error recovery, not just success
  • Spec-driven testing - Test user goals, not features
  • Ignoring time - Measure how long journeys take
  • Desktop-only - Test mobile journeys separately
  • Skipping emotions - Consider user frustration points
  • No metrics - Track journey completion and drop-off
  • Static journeys - Update as user behavior evolves

  • 仅测试理想路径 - 测试错误恢复,而非仅成功场景
  • 规格驱动测试 - 测试用户目标,而非功能
  • 忽略时长 - 测量旅程完成时间
  • 仅支持桌面端 - 单独测试移动端旅程
  • 忽略情绪 - 考虑用户的沮丧点
  • 无指标跟踪 - 跟踪旅程完成率与流失率
  • 静态旅程 - 随用户行为变化更新旅程

Quick Reference

快速参考

Journey Priorities

旅程优先级

PriorityCriteriaTest Frequency
CriticalRevenue, core valueEvery deploy
HighDaily user actionsDaily
MediumWeekly featuresWeekly
LowEdge casesOn change
优先级判定标准测试频率
核心营收、核心价值相关每次部署
用户日常操作每日
每周使用的功能每周
边缘场景变更时测试

Package.json Scripts

Package.json脚本

json
{
  "scripts": {
    "test:journeys": "playwright test e2e/tests/journeys/",
    "test:journeys:critical": "playwright test e2e/tests/journeys/critical/",
    "test:journeys:report": "playwright show-report"
  }
}
json
{
  "scripts": {
    "test:journeys": "playwright test e2e/tests/journeys/",
    "test:journeys:critical": "playwright test e2e/tests/journeys/critical/",
    "test:journeys:report": "playwright show-report"
  }
}

Journey Documentation Checklist

旅程文档检查清单

  • User goal clearly stated
  • All steps documented
  • Success criteria per step
  • Error scenarios covered
  • Recovery paths defined
  • Metrics identified
  • E2E test linked
  • 用户目标清晰表述
  • 所有步骤已记录
  • 各步骤定义成功标准
  • 覆盖错误场景
  • 定义恢复路径
  • 确定跟踪指标
  • 关联端到端测试