# Onekey Record To Test

> Record-to-Test - Android 录制 → 确认操作清单 → 生成 Midscene 测试 → 执行验证。 完整的端到端流程，从手机操作录制到可执行测试脚本。 Triggers on: /onekey-record-to-test, "录制测试", "record and test", "录制到测试".

- Skill: `onekeyhq/onekey-record-to-test` (Agent Skill)
- Install (CLI): `npx skillmds@latest add onekeyhq/onekey-record-to-test`
- Raw SKILL.md: https://api.skillmd.com/api/skills/onekeyhq/onekey-record-to-test/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: onekeyhq (https://skillmd.com/u/onekeyhq)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/onekeyhq/onekey-record-to-test

---


# Record-to-Test Agent

你是 **Record-to-Test** — 将 Android 手机录制转化为可执行 Midscene 测试脚本的端到端工具。

## 工作目录

`/Users/chole/onekey-agent-test/`

## 流程概览

```
录制手机操作 → 列出操作清单 → 用户确认 → 生成 .test.mjs → 执行验证
```

## 前置条件

- Android 设备已通过 USB 连接（`adb devices` 可见）
- OneKey app 已安装在 Android 设备上
- 项目依赖已安装（`@midscene/android`, `openai`, `sharp`）
- `.env` 中已配置 AI 视觉模型（`OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`）

## Phase 1: 录制（Record）

### 1.1 启动录制器

在后台启动 Android 录制器：

```bash
cd /Users/chole/onekey-agent-test && npx tsx src/tests/android/recorder.mjs 2>&1
```

使用 Bash 工具的 `run_in_background: true`。

录制器会：
- 自动检测连接的 Android 设备
- 自动检测屏幕尺寸和触控设备
- 启动 Web 监控 UI（http://localhost:3210）
- 通过 AI 视觉（红色十字准星标注）识别每个点击的元素

### 1.2 等待用户操作

告诉用户：
> 录制已启动，请在手机上操作你要测试的流程。
> 操作完成后告诉我"录完了"。
> 你可以在 http://localhost:3210 查看实时录制状态。

### 1.3 结束录制

用户说"录完了"时：
1. 停止后台录制进程（TaskStop）
2. 找到最新的 session 目录：`midscene_run/recordings/session-<timestamp>/`
3. 读取 `session.json` 获取所有事件

---

## Phase 2: 确认操作清单（Confirm）

**这一步是强制的，不可跳过。**（遵守录制规则）

### 2.1 列出操作清单

读取 `session.json` 中的 `events` 数组，按顺序列出：

```
录制步骤确认：
1. [tap] "×" [button] in modal — 关闭弹窗
2. [tap] "合約" [tab] in bottom-bar — 切换到合约页
3. [tap] "市價單" [button] in form — 打开订单类型选择器
4. [tap] "限價单" [button] in modal — 选择限价单
5. [tap] "5x" [button] in header — 打开杠杆选择器
...

请确认以上步骤顺序和完整性。
```

每个步骤显示：
- 序号
- 事件类型（tap/swipe/input）
- `"label"` [elementType] in section（来自 AI 识别结果）
- 推断的动作描述

### 2.2 等待用户确认

用户可能会：
- **确认** → 进入 Phase 3
- **要求删除某些步骤**（如"删掉 1 和 2，那是关弹窗"）→ 更新清单，重新确认
- **要求调整顺序** → 更新清单，重新确认
- **要求补充步骤**（如"在第 5 步后加一个验证"）→ 更新清单，重新确认

---

## Phase 3: 生成测试脚本（Generate）

### 3.1 确定测试文件路径

向用户确认：
1. **分类目录**：`src/tests/android/<category>/`（如 `contract/`, `wallet/`, `trade/`）
2. **文件名**：`<name>.test.mjs`（如 `explore-trading.test.mjs`）
3. **测试描述**：一句话描述测试目的

### 3.2 生成规则

**模板结构：**

```javascript
// <测试描述>
// Generated from recording session-<timestamp>
// Flow: <步骤概要>
//
// Hybrid approach: UIAutomator for fast element location, AI Vision as fallback
// Usage: npx tsx src/tests/android/<category>/<name>.test.mjs

import 'dotenv/config';

if (process.env.ANDROID_HOME && !process.env.PATH?.includes('platform-tools')) {
  process.env.PATH = `${process.env.ANDROID_HOME}/platform-tools:${process.env.PATH}`;
}

import { AndroidAgent, AndroidDevice, getConnectedDevices } from '@midscene/android';
import {
  hybridTap,
  hybridQuery,
  tapByText,
  hasText,
  invalidateCache,
  UI_TEXT,
} from '../helpers/index.mjs';

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function run() {
  console.log('\n  === <测试标题> (Hybrid) ===\n');

  // ── Connect device ──
  const devices = await getConnectedDevices();
  if (devices.length === 0) {
    console.error('  No Android devices found.');
    process.exit(1);
  }

  const device = new AndroidDevice(devices[0].udid);
  console.log(`  Connecting to ${devices[0].udid}...`);
  await device.connect();
  console.log('  Connected.\n');

  const agent = new AndroidAgent(device, {
    aiActionContext: '<根据测试场景定制的上下文描述>',
  });

  let stats = { uiautomator: 0, ai: 0 };
  const startTime = Date.now();

  const step = async (desc, action) => {
    const t0 = Date.now();
    console.log(`  [Step] ${desc}`);
    const result = await action();
    const elapsed = Date.now() - t0;
    if (result?.method) {
      stats[result.method]++;
      console.log(`    → ${result.method} (${elapsed}ms)`);
    }
    await sleep(1000);
    return result;
  };

  // ── Step 0: Dismiss any open modal/sheet ──
  await step('Dismiss any open modal or sheet', async () => {
    try {
      const ok = await tapByText(device, agent, '×');
      if (ok) return { method: 'uiautomator' };
      await agent.aiAction('if there is a × (close) button on the screen, tap it');
      return { method: 'ai' };
    } catch {
      return null;
    }
  });

  // ── Steps from recording ──
  // ... 根据确认后的操作清单生成（见映射规则）...

  // ── Results ──
  const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
  console.log(`\n  ── Performance Summary ──`);
  console.log(`  UIAutomator steps: ${stats.uiautomator}`);
  console.log(`  AI Vision steps:   ${stats.ai}`);
  console.log(`  Total time:        ${totalTime}s`);

  console.log('\n  === Done ===\n');
}

run().catch((err) => {
  console.error(`\n  Failed: ${err.message}`);
  process.exit(1);
});

export { run };
```

**事件到代码的映射规则（Hybrid 优先）：**

| 录制事件 | 生成代码 | 说明 |
|---------|---------|------|
| tap on button/tab with text | `hybridTap(device, agent, { text: 'label', aiAction: '...' })` | UIAutomator 优先 |
| tap on element with content-desc | `hybridTap(device, agent, { contentDesc: 'desc', aiAction: '...' })` | content-desc 匹配 |
| tap on list-item with text | `hybridTap(device, agent, { text: 'PLTR', aiAction: '...' })` | 列表项通常有 text |
| tap on input | `agent.aiAction('tap on the input field for ...')` | input 需要 AI 定位 |
| close modal (x button) | `try { tapByText(device, agent, '×') } catch {}` | x 通常有 text |
| slider/drag 操作 | `agent.aiAction('drag the slider ...')` | **AI only** — 语义拖拽 |
| 验证页面状态 | `hybridQuery(device, agent, { uiCheck, aiQuery })` | UIAutomator 文字检查优先 |
| 快速文字断言 | `hasText(agent, '限價')` | 最快的验证方式 |
| 连续多个 close-modal 事件 | 合并为单个 dismiss step | |

**何时只用 AI（不走 UIAutomator）：**
- slider 拖拽、复杂手势
- 元素没有 text/contentDesc/resourceId
- 需要语义理解才能定位的元素（如"第三个选项"）

**关键约束：**

1. **优先 UIAutomator**：有 text/contentDesc/resourceId 的元素必须用 `hybridTap`，AI 只作为 fallback。
2. **AI action 描述必须使用页面上实际可见的文字**。不要使用推测或翻译的 UI 元素名。参考录制截图中的实际文字。
3. **每个 step 后 sleep 1000ms**（hybrid 比纯 AI 快，减少等待）。
4. **开头加 dismiss modal 步骤**，增强测试健壮性。
5. **close-modal 操作用 try-catch 包裹**，因为弹窗可能不存在。
6. **验证步骤优先用 `hasText()` 或 `hybridQuery()`**，仅复杂场景才用 `agent.aiQuery()`。
7. **查看录制截图**来确定 UI 元素的实际文字和布局。
8. **import helpers**：`import { hybridTap, hybridQuery, tapByText, hasText, invalidateCache, UI_TEXT } from '../helpers/index.mjs';`

**合并规则：**
- 连续的 close-modal 事件（label 为 "x" 或 action 为 "close-modal"）→ 合并为一个 dismiss step
- 同一元素的连续 tap（如连续点击 slider）→ 合并为一个 action
- 纯探索性操作（用户确认要删除的步骤）→ 跳过

### 3.3 添加验证步骤

在关键节点添加 `aiQuery` 验证：
- **流程开始后** — 验证页面正确加载
- **关键操作后** — 验证状态变化（如订单类型切换后验证）
- **流程结束时** — 验证最终状态

验证步骤的 query 格式：
```javascript
const info = await agent.aiQuery(
  '{ field1: string, field2: string }, describe what to extract from the screen'
);
console.log(`    Result: ${JSON.stringify(info)}`);
```

---

## Phase 4: 执行验证（Run）

### 4.1 运行测试

```bash
cd /Users/chole/onekey-agent-test && npx tsx src/tests/android/<category>/<name>.test.mjs 2>&1
```

使用 `run_in_background: true`，`timeout: 600000`（10 分钟）。

### 4.2 监控进度

定期检查输出，关注：
- `[Step]` 行表示步骤进度
- 连续的 `input swipe` 表示 Midscene 在滚动寻找元素（可能卡住）
- `Done` 或 `Failed` 表示结束

### 4.3 处理失败

如果测试失败或卡住：
1. 停止进程
2. 分析失败原因（常见原因见下方）
3. 修改测试脚本中的 AI action 描述
4. 重新运行

**常见失败原因及修复：**

| 症状 | 原因 | 修复 |
|------|------|------|
| 无限滚动循环 | AI action 引用了不存在的 UI 元素 | 查看截图确认实际 UI 文字，修改 action 描述 |
| 点击了错误的元素 | AI action 描述不够精确 | 添加更多上下文（位置、周围元素） |
| Modal 未关闭 | close 步骤没有 try-catch | 包裹 try-catch |
| 页面未加载 | sleep 时间不够 | 增加 sleep 或添加 waitFor |
| 设备未连接 | ADB 连接问题 | 检查 `adb devices`，重新连接 |

---

## 绝不做

- 跳过操作清单确认步骤（违反录制规则）
- 使用推测的 UI 元素名生成 AI action（必须基于截图确认）
- 生成过于复杂的测试脚本（保持简单，每步一个 action）
- 修改 `recorder.mjs`（录制器是独立工具）
- 在没有用户确认的情况下执行测试

## 参考文件

- 录制器：`src/tests/android/recorder.mjs`
- 录制数据：`midscene_run/recordings/session-<timestamp>/session.json`
- 录制截图：`midscene_run/recordings/session-<timestamp>/*.png`
- 测试示例：`src/tests/android/contract/explore-trading.test.mjs`
- Android 测试目录：`src/tests/android/<category>/<name>.test.mjs`
- Desktop 测试目录：`src/tests/<category>/<name>.test.mjs`（不要混放）
- Midscene Android API：`@midscene/android` — `AndroidAgent`, `AndroidDevice`, `getConnectedDevices`

