Loading...
Loading...
Compare original and translation side by side
_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.mdundefinedundefined| Attribute | Value |
|---|---|
| Priority | Critical / High / Medium |
| User Type | New / Returning / Admin |
| Frequency | Daily / Weekly / One-time |
| Success Metric | Conversion rate, time to complete, drop-off rate |
| 属性 | 值 |
|---|---|
| 优先级 | 核心 / 高 / 中 |
| 用户类型 | 新用户 / 回头客 / 管理员 |
| 发生频率 | 每日 / 每周 / 一次性 |
| 成功指标 | 转化率、完成时长、流失率 |
"I want to [goal] so that I can [benefit]."
"我想要[达成目标],这样我就能[获得收益]。"
e2e/tests/journeys/[name].spec.ts
---e2e/tests/journeys/[name].spec.ts
---undefinedundefined| Attribute | Value |
|---|---|
| Priority | Critical |
| User Type | New |
| Frequency | One-time |
| Success Metric | % reaching "aha moment" within 5 min |
| 属性 | 值 |
|---|---|
| 优先级 | 核心 |
| 用户类型 | 新用户 |
| 发生频率 | 一次性 |
| 成功指标 | 5分钟内达到"惊喜时刻"的用户占比 |
"I want to try this product quickly to see if it solves my problem."
"我想快速试用这款产品,看看它能否解决我的问题。"
signup-existing-email.spec.tssignup-existing-email.spec.tssocial-login-failure.spec.tssocial-login-failure.spec.ts
---
---undefinedundefined| Attribute | Value |
|---|---|
| Priority | Critical (Revenue) |
| User Type | Any |
| Frequency | Variable |
| Success Metric | Checkout completion rate |
| 属性 | 值 |
|---|---|
| 优先级 | 核心(营收相关) |
| 用户类型 | 所有用户 |
| 发生频率 | 不定 |
| 成功指标 | 结账完成率 |
"I want to pay quickly and securely without surprises."
"我想快速、安全地完成支付,没有意外费用。"
payment-declined-recovery.spec.tspayment-declined-recovery.spec.tscheckout-session-timeout.spec.ts
---checkout-session-timeout.spec.ts
---// 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);
});
});// 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 }) => {
// 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);
});
});// 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);
});
});undefinedundefinedundefinedundefined// 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();
}
}// 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;
}
// 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,
});// 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,
});Step 1: Show basic options only
Step 2: "Advanced" expands more options
Step 3: Expert mode unlocks everything步骤1:仅显示基础选项
步骤2:"高级"选项展开更多功能
步骤3:专家模式解锁全部功能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步骤1:欢迎页 + 单项选择
步骤2:核心偏好设置
步骤3:可选集成(可跳过)
步骤4:带引导的首次操作
步骤5:成功提示 + 移除引导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步骤1:识别回头用户
步骤2:恢复之前的状态
步骤3:告知用户之前的情况
步骤4:提供清晰的前进路径
步骤5:完成原目标| Priority | Criteria | Test Frequency |
|---|---|---|
| Critical | Revenue, core value | Every deploy |
| High | Daily user actions | Daily |
| Medium | Weekly features | Weekly |
| Low | Edge cases | On change |
| 优先级 | 判定标准 | 测试频率 |
|---|---|---|
| 核心 | 营收、核心价值相关 | 每次部署 |
| 高 | 用户日常操作 | 每日 |
| 中 | 每周使用的功能 | 每周 |
| 低 | 边缘场景 | 变更时测试 |
{
"scripts": {
"test:journeys": "playwright test e2e/tests/journeys/",
"test:journeys:critical": "playwright test e2e/tests/journeys/critical/",
"test:journeys:report": "playwright show-report"
}
}{
"scripts": {
"test:journeys": "playwright test e2e/tests/journeys/",
"test:journeys:critical": "playwright test e2e/tests/journeys/critical/",
"test:journeys:report": "playwright show-report"
}
}