用户旅程技能 user-journeys

定义和测试真实用户体验的技能,专注于用户实际的导航流程、情感状态、错误恢复和现实世界条件。

测试 0 次安装 0 次浏览 更新于 3/5/2026

用户旅程技能

加载: base.md + playwright-testing.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

旅程模板

# 旅程:[名称]

## 概览
| 属性 | 值 |
|-----------|-------|
| **优先级** | 临界 / 高 / 中 |
| **用户类型** | 新 / 返回 / 管理员 |
| **频率** | 每日 / 每周 / 一次性 |
| **成功指标** | 转化率,完成时间,退出率 |

## 用户目标
用户想要完成什么?从他们的视角写。

> "我想[目标],这样我就可以[好处]。"

## 前提条件
- 用户状态(登录,有订阅,首次访问)
- 数据状态(购物车中有商品,有团队成员)
- 环境(移动,桌面,慢速连接)

## 旅程步骤

### 第1步:[入口点]
**用户操作:**用户做什么
**系统响应:**他们应该看到/体验什么
**成功标准:**
- [ ] 页面在<2秒内加载
- [ ] 主要CTA立即可见
- [ ] 用户知道下一步该做什么

**潜在摩擦:**
- 慢速加载时间 → 显示骨架/加载器
- 不清晰的CTA → A/B测试副本变体

---

### 第2步:[下一个行动]
**用户操作:**...
**系统响应:**...
**成功标准:**
- [ ] ...

**潜在摩擦:**
- ...

---

## 错误场景

### E1:[错误名称]
**触发器:**什么导致这个错误
**用户看到:**错误消息/状态
**恢复路径:**用户如何回到正轨
**测试:**如何验证恢复工作

## 跟踪指标
- 完成旅程的时间
- 每个步骤的退出率
- 错误率和恢复率
- 用户满意度(如果有调查)

## E2E测试参考
链接到Playwright测试:`e2e/tests/journeys/[name].spec.ts`

临界旅程示例

注册到首次价值

# 旅程:注册到首次价值

## 概览
| 属性 | 值 |
|-----------|-------|
| **优先级** | 临界 |
| **用户类型** | 新 |
| **频率** | 一次性 |
| **成功指标** | %在5分钟内达到"aha时刻" |

## 用户目标
> "我想快速尝试这个产品,看看它是否解决了我的问题。"

## 前提条件
- 首次访问网站
- 没有账户
- 来自着陆页或广告

## 旅程步骤

### 第1步:着陆页
**用户操作:**点击"开始免费"或"立即尝试"
**系统响应:**注册表单出现(模态或新页面)
**成功标准:**
- [ ] CTA在折叠上方可见
- [ ] 没有分散注意力的元素
- [ ] 清晰的价值主张可见

**潜在摩擦:**
- 表单字段太多 → 减少到只有电子邮件+密码
- 缺少社交登录 → 添加Google/GitHub选项

### 第2步:账户创建
**用户操作:**输入电子邮件和密码(或使用社交登录)
**系统响应:**
- 创建账户
- 发送验证电子邮件(不阻塞在它上面)
- 重定向到入门

**成功标准:**
- [ ] 账户在<3秒内创建
- [ ] 没有电子邮件验证墙(稍后验证)
- [ ] 显示清晰的下一步

**潜在摩擦:**
- 电子邮件已存在 → 提供登录链接
- 密码弱 → 在线显示要求,而不是提交后

### 第3步:入门(快速获胜)
**用户操作:**完成1-2个设置问题
**系统响应:**
- 个性化体验
- 显示进度指示器
- 引导到第一个行动

**成功标准:**
- [ ] 最多3个问题
- [ ] 可用跳过选项
- [ ] <60秒总计

**潜在摩擦:**
- 问题太多 → 用户放弃
- 没有跳过选项 → 用户感觉被困

### 第4步:首次价值(Aha时刻)
**用户操作:**完成核心行动(创建第一个X,看到第一个结果)
**系统响应:**
- 庆祝成功
- 显示交付的价值
- 建议下一步

**成功标准:**
- [ ] 用户体验到核心价值
- [ ] 完成感觉有回报
- [ ] 清晰的继续路径

## 错误场景

### E1:电子邮件已注册
**触发器:**用户尝试现有电子邮件
**用户看到:**"已经有账户?登录或重置密码"
**恢复路径:**点击登录或重置
**测试:**`signup-existing-email.spec.ts`

### E2:社交登录失败
**触发器:**OAuth提供商错误
**用户看到:**"无法连接。尝试电子邮件注册或再试一次。"
**恢复路径:**显示电子邮件注册表单作为回退
**测试:**`social-login-failure.spec.ts`

## 跟踪指标
- 注册 → 第一次价值:目标<5分钟
- 每个步骤的退出率
- 社交与电子邮件注册比例
- 入门跳过率

检查购买

# 旅程:检查购买

## 概览
| 属性 | 值 |
|-----------|-------|
| **优先级** | 临界(收入) |
| **用户类型** | 任何 |
| **频率** | 可变 |
| **成功指标** | 检查完成率 |

## 用户目标
> "我想快速安全地支付,没有惊喜。"

## 旅程步骤

### 第1步:购物车审查
**用户操作:**在检查之前查看购物车
**系统响应:**
- 显示所有商品及其图片、价格
- 显示小计、税费、运费
- 清晰的"检查"CTA

**成功标准:**
- [ ] 稍后不显示隐藏费用
- [ ] 容易修改数量
- [ ] 保存的商品可见

### 第2步:检查开始
**用户操作:**点击"检查"
**系统响应:**
- 显示检查表单或重定向到支付
- 进度指示器(第1步/3)
- 订单摘要侧边栏

**成功标准:**
- [ ] 访客检查选项
- [ ] 快速检查(Apple/Google Pay)突出显示
- [ ] 如果登录,表单字段预填充

### 第3步:支付
**用户操作:**输入支付信息
**系统响应:**
- 安全输入字段(Stripe/支付提供商)
- 实时验证
- 清晰的"支付$XX"按钮

**成功标准:**
- [ ] 卡片验证内联,而不是提交后
- [ ] 多种支付选项
- [ ] 安全指示器可见

### 第4步:确认
**用户操作:**提交支付
**系统响应:**
- 处理指示器
- 成功页面显示订单详情
- 发送电子邮件确认

**成功标准:**
- [ ] 确认在5秒内
- [ ] 订单号清晰可见
- [ ] 下一步清晰(运输、访问等)

## 错误场景

### E1:支付被拒绝
**触发器:**卡被处理器拒绝
**用户看到:**"支付被拒绝。请尝试另一张卡。"
**恢复路径:**
- 停留在支付步骤
- 预填充其他字段
- 提供替代支付方式
**测试:**`payment-declined-recovery.spec.ts`

### E2:检查期间会话超时
**触发器:**用户离开太久
**用户看到:**购物车保留,需要重新认证
**恢复路径:**
- 快速登录
- 返回到同一检查步骤
- 购物车内容完整
**测试:**`checkout-session-timeout.spec.ts`

使用Playwright进行旅程测试

旅程测试结构

// 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);
  });
});

错误恢复测试

// 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);
  });
});

用户体验验证

每个旅程步骤的UX检查表

## UX验证检查表

### 清晰度
- [ ] 用户知道他们在哪里(面包屑,进度)
- [ ] 用户知道下一步该做什么(清晰的CTA)
- [ ] 用户知道刚才发生了什么(反馈)

### 速度
- [ ] 页面加载<2秒
- [ ] 操作完成<3秒
- [ ] 显示长时间操作的进度

### 宽容
- [ ] 错误容易被撤销
- [ ] 错误解释出了什么问题
- [ ] 清晰的恢复路径

### 可访问性
- [ ] 键盘导航工作
- [ ] 屏幕阅读器宣布变化
- [ ] 焦点管理正确
- [ ] 颜色对比度足够

### 移动
- [ ] 触摸目标>=44px
- [ ] 没有水平滚动
- [ ] 表单不意外缩放
- [ ] 在慢速3G上工作

自动化UX检查

// 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();
  }
}

旅程指标仪表板

用这些指标跟踪旅程的健康状况:

// 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,
});

常见旅程模式

逐步披露旅程

用户首先看到简单的视图,需要时再揭示复杂性。

第1步:只显示基本选项
第2步:"高级"展开更多选项
第3步:专家模式解锁一切

引导设置旅程

手把手引导新用户完成初始配置。

第1步:欢迎+单一选择
第2步:核心偏好
第3步:可选集成(可跳过)
第4步:第一次行动指导
第5步:成功+移除训练轮

恢复旅程

用户在失败或放弃后返回。

第1步:识别返回用户
第2步:恢复先前状态
第3步:承认发生了什么
第4步:提供清晰的前进路径
第5步:完成原始目标

反模式

  • 仅快乐路径 - 测试错误恢复,不仅仅是成功
  • 规格驱动测试 - 测试用户目标,而不是功能
  • 忽略时间 - 测量旅程需要多长时间
  • 仅限桌面 - 分别测试移动旅程
  • 跳过情感 - 考虑用户的挫败点
  • 没有指标 - 跟踪旅程完成和退出
  • 静态旅程 - 随着用户行为的演变而更新

快速参考

旅程优先级

优先级 标准 测试频率
临界 收入,核心价值 每次部署
日常用户操作 每日
每周功能 每周
边缘情况 变更时

package.json脚本

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

旅程文档检查表

  • [ ] 用户目标清晰说明
  • [ ] 所有步骤记录
  • [ ] 每个步骤的成功标准
  • [ ] 覆盖错误场景
  • [ ] 定义恢复路径
  • [ ] 识别指标
  • [ ] 链接E2E测试