software-design-test 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/LICENSE +21 -0
  3. package/README.md +275 -0
  4. package/cordis.patch.yml +23 -0
  5. package/docs/FRAMEWORK.zh-en.md +149 -0
  6. package/docs/INSTALL.zh-en.md +226 -0
  7. package/docs/USAGE.zh-en.md +295 -0
  8. package/icon.svg +21 -0
  9. package/lib/index.js +197 -0
  10. package/lib/self-check.js +313 -0
  11. package/locale/en.json +6 -0
  12. package/locale/zh.json +6 -0
  13. package/package.json +80 -0
  14. package/scripts/capture.mjs +318 -0
  15. package/scripts/guard.mjs +311 -0
  16. package/scripts/report.mjs +484 -0
  17. package/scripts/session.mjs +709 -0
  18. package/scripts/verify.mjs +205 -0
  19. package/skills/observed-test-plan/MATRIX.md +99 -0
  20. package/skills/observed-test-plan/PERMISSIONS.md +109 -0
  21. package/skills/observed-test-plan/PLAN-TEMPLATE.md +93 -0
  22. package/skills/observed-test-plan/SKILL.md +135 -0
  23. package/skills/observed-ui-test/BANNED-INPUTS.md +161 -0
  24. package/skills/observed-ui-test/EVIDENCE.md +113 -0
  25. package/skills/observed-ui-test/FRAMEWORK.md +181 -0
  26. package/skills/observed-ui-test/LEVELS.md +137 -0
  27. package/skills/observed-ui-test/REPORT-TEMPLATE.md +111 -0
  28. package/skills/observed-ui-test/SKILL.md +210 -0
  29. package/skills/software-design-test/DEFECTS.md +205 -0
  30. package/skills/software-design-test/HEURISTICS.md +236 -0
  31. package/skills/software-design-test/PERSONAS-SCENARIOS.md +159 -0
  32. package/skills/software-design-test/SKILL.md +211 -0
  33. package/skills/software-design-test/SOURCES.md +118 -0
  34. package/skills/software-design-test/TEST-CONTENT.md +257 -0
  35. package/skills/software-design-test/WORKFLOW.md +293 -0
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * verify.mjs — offline, read-only verification of this plugin.
4
+ *
5
+ * It never touches the screen, the network or an input device. It checks that:
6
+ * 1. the plugin mounts and publishes every skill through the ctx.skills seam;
7
+ * 2. every SKILL.md has valid, bilingual frontmatter;
8
+ * 3. every relative markdown link resolves;
9
+ * 4. no shipped code contains an input-injection API;
10
+ * 5. the session / gate / finding / guard / report CLIs work end to end in a temp directory.
11
+ *
12
+ * 离线自检:挂载、技能元数据、相对链接、注入 API 扫描、脚本端到端冒烟。
13
+ *
14
+ * Usage / 用法: node scripts/verify.mjs
15
+ */
16
+
17
+ import { spawnSync } from 'node:child_process'
18
+ import { mkdtempSync, rmSync, existsSync, readFileSync } from 'node:fs'
19
+ import { tmpdir } from 'node:os'
20
+ import { join } from 'node:path'
21
+ import { fileURLToPath } from 'node:url'
22
+
23
+ import {
24
+ checkContentRequirements,
25
+ checkRelativeLinks,
26
+ checkSkills,
27
+ mentionsGuardObservation,
28
+ scanForInjectionApis,
29
+ } from '../lib/self-check.js'
30
+ import { apply } from '../lib/index.js'
31
+
32
+ const packageRoot = fileURLToPath(new URL('..', import.meta.url))
33
+ const results = []
34
+
35
+ async function check(name, run) {
36
+ try {
37
+ const detail = await run()
38
+ results.push({ name, ok: true, detail: detail ?? '' })
39
+ } catch (error) {
40
+ results.push({ name, ok: false, detail: error.message })
41
+ }
42
+ }
43
+
44
+ function assert(condition, message) {
45
+ if (!condition) throw new Error(message)
46
+ }
47
+
48
+ const node = process.execPath
49
+ const run = (args, options = {}) =>
50
+ spawnSync(node, args, { encoding: 'utf8', cwd: packageRoot, ...options })
51
+
52
+ await check('mount: plugin publishes every skill through ctx.skills', async () => {
53
+ let provider
54
+ const ctx = { skills: { registerProvider: (factory) => { provider = factory() } } }
55
+ apply(ctx, {})
56
+ assert(provider !== undefined, 'registerProvider was not called')
57
+ const candidates = await provider.list()
58
+ const names = candidates.map((candidate) => candidate.name)
59
+ assert(candidates.length >= 3, `expected at least 3 skills, got ${names.join(', ')}`)
60
+ for (const skill of ['observed-ui-test', 'observed-test-plan', 'software-design-test']) {
61
+ assert(names.includes(skill), `${skill} skill missing`)
62
+ }
63
+ for (const candidate of candidates) {
64
+ assert('rank' in candidate, 'rank should exist on the internal candidate')
65
+ assert(candidate.provider === 'software-design-test', 'wrong provider name')
66
+ }
67
+ return `${names.join(', ')} (${candidates.length} skills)`
68
+ })
69
+
70
+ await check('mount: provider.get returns body without leaking rank/locator', async () => {
71
+ let provider
72
+ apply({ skills: { registerProvider: (factory) => { provider = factory() } } }, {})
73
+ const candidates = await provider.list()
74
+ for (const candidate of candidates) {
75
+ const loaded = await provider.get(candidate, {})
76
+ assert(typeof loaded.content === 'string' && loaded.content.length > 100, `${candidate.name}: empty body`)
77
+ assert(!('locator' in loaded), `${candidate.name}: locator leaked`)
78
+ assert(!('rank' in loaded), `${candidate.name}: rank leaked`)
79
+ }
80
+ return 'bodies load, internals stay internal'
81
+ })
82
+
83
+ await check('skills: frontmatter is valid and bilingual', () => {
84
+ const { problems, skills } = checkSkills(packageRoot)
85
+ assert(problems.length === 0, problems.join('\n '))
86
+ return skills.map((skill) => skill.name).join(', ')
87
+ })
88
+
89
+ await check('links: every relative markdown link resolves', () => {
90
+ const problems = checkRelativeLinks(packageRoot)
91
+ assert(problems.length === 0, problems.join('\n '))
92
+ return 'all relative links resolve'
93
+ })
94
+
95
+ await check('ban: shipped code contains no input-injection API', () => {
96
+ const problems = scanForInjectionApis(packageRoot)
97
+ assert(problems.length === 0, problems.join('\n '))
98
+ return 'lib/ and scripts/ are injection-free'
99
+ })
100
+
101
+ await check('content: method requirements are documented bilingually', () => {
102
+ const problems = checkContentRequirements(packageRoot)
103
+ assert(problems.length === 0, problems.join('\n '))
104
+ assert(mentionsGuardObservation(packageRoot), 'guard.mjs observation wording missing')
105
+ return 'three modes, gate, banned list and guard are documented'
106
+ })
107
+
108
+ await check('cli: session init / gate / finding / guard / report end to end', () => {
109
+ const dir = mkdtempSync(join(tmpdir(), 'observed-ui-test-'))
110
+ try {
111
+ const target = join(dir, 'session')
112
+
113
+ let result = run(['scripts/session.mjs', 'init', target, '--platform', 'macos', '--app', 'Demo'])
114
+ assert(result.status === 0, `init failed: ${result.stderr}`)
115
+ for (const file of ['session.json', 'permissions.md', 'elements.md', 'matrix.md', 'findings.jsonl', 'report.md']) {
116
+ assert(existsSync(join(target, file)), `init did not create ${file}`)
117
+ }
118
+ assert(existsSync(join(target, 'evidence')), 'init did not create evidence/')
119
+
120
+ // The gate blocks while capture permission is missing.
121
+ result = run(['scripts/session.mjs', 'gate', target, '--mouse', 'yes', '--keyboard', 'L1', '--screen-recording', 'no', '--screenshot', 'no', '--compliance', 'yes'])
122
+ assert(result.status === 1, `gate should block without capture permission, got ${result.status}`)
123
+ assert(result.stdout.includes('NOT CONFIRMED'), 'gate did not report NOT CONFIRMED')
124
+
125
+ // Denied mouse is fatal.
126
+ result = run(['scripts/session.mjs', 'gate', target, '--mouse', 'no', '--keyboard', 'L1', '--screen-recording', 'yes', '--screenshot', 'yes', '--compliance', 'yes'])
127
+ assert(result.status === 1, 'gate should block when the mouse is denied')
128
+
129
+ // Full answer passes.
130
+ result = run(['scripts/session.mjs', 'gate', target, '--mouse', 'yes', '--keyboard', 'L2', '--screen-recording', 'yes', '--screenshot', 'yes', '--microphone', 'no', '--os-permission', 'yes', '--cursor', 'yes', '--compliance', 'yes', '--data-boundary', 'demo account only'])
131
+ assert(result.status === 0, `gate should confirm: ${result.stdout}${result.stderr}`)
132
+ assert(result.stdout.includes('CONFIRMED'), 'gate did not confirm')
133
+
134
+ result = run(['scripts/session.mjs', 'status', target])
135
+ assert(result.status === 0 && result.stdout.includes('已通过'), 'status did not report a confirmed gate')
136
+
137
+ const finding = JSON.stringify({
138
+ level: 'L1',
139
+ element: 'E-07 toolbar export button',
140
+ title_zh: '导出按钮点击无反应',
141
+ title_en: 'Export button does nothing',
142
+ steps_zh: '移动鼠标到导出按钮,单击',
143
+ steps_en: 'Move the mouse to Export, click',
144
+ expected_zh: '弹出导出面板',
145
+ expected_en: 'Export dialog opens',
146
+ actual_zh: '无任何反应,无按下态',
147
+ actual_en: 'Nothing happens, no pressed state',
148
+ severity: 'S2',
149
+ kind: 'defect',
150
+ modes: 'L1 fail / L2 fail / L3 pass',
151
+ evidence: ['evidence/x.png'],
152
+ })
153
+ result = run(['scripts/session.mjs', 'finding', target, '--json', finding])
154
+ assert(result.status === 0, `finding failed: ${result.stderr}`)
155
+ assert(result.stdout.includes('F-001'), 'finding did not get an id')
156
+
157
+ result = run(['scripts/guard.mjs', 'scan', target])
158
+ assert(result.status === 0 || result.status === 1, `guard scan crashed: ${result.stderr}`)
159
+ assert(result.stdout.includes('输入注入观察'), 'guard scan output missing')
160
+ assert(existsSync(join(target, 'evidence', 'compliance.jsonl')), 'guard did not log compliance')
161
+
162
+ result = run(['scripts/report.mjs', 'build', target])
163
+ assert(result.status === 0, `report failed: ${result.stderr}`)
164
+ const report = readFileSync(join(target, 'report.md'), 'utf8')
165
+ assert(report.includes('Observed UI Test Report'), 'report missing title')
166
+ assert(report.includes('F-001'), 'report missing the finding')
167
+ assert(report.includes('导出按钮点击无反应'), 'report missing the Chinese title')
168
+ assert(report.includes('输入注入监控'), 'report missing the injection-watch section')
169
+ assert(report.includes('No internal pointer directives were used'), 'report missing compliance statement')
170
+
171
+ return 'init → gate(block/confirm) → finding → guard → report all behave'
172
+ } finally {
173
+ rmSync(dir, { recursive: true, force: true })
174
+ }
175
+ })
176
+
177
+ await check('cli: capture.mjs exposes no input subcommand and checks availability', () => {
178
+ const result = run(['scripts/capture.mjs', 'check'])
179
+ assert(result.status === 0, `capture check failed: ${result.stderr}`)
180
+ assert(result.stdout.includes('只读捕获自检'), 'capture check output missing')
181
+ const source = readFileSync(join(packageRoot, 'scripts/capture.mjs'), 'utf8')
182
+ for (const forbidden of ['SendInput', 'CGEventPost', 'adb shell input', 'input tap']) {
183
+ assert(!source.includes(forbidden), `capture.mjs must not mention ${forbidden}`)
184
+ }
185
+ for (const command of ['shot', 'record', 'check']) {
186
+ assert(source.includes(`command === '${command}'`), `capture.mjs lost its ${command} command`)
187
+ }
188
+ assert(!/command === 'input'/.test(source), 'capture.mjs must not gain an input command')
189
+ return 'read-only capture CLI verified'
190
+ })
191
+
192
+ const failed = results.filter((result) => !result.ok)
193
+ for (const result of results) {
194
+ process.stdout.write(`${result.ok ? 'PASS' : 'FAIL'} ${result.name}\n`)
195
+ if (result.detail) {
196
+ for (const line of String(result.detail).split('\n')) {
197
+ process.stdout.write(` ${line}\n`)
198
+ }
199
+ }
200
+ }
201
+ process.stdout.write(
202
+ `\n${results.length - failed.length}/${results.length} checks passed` +
203
+ ` — 注入能力: 无 / capability to inject input: none\n`,
204
+ )
205
+ process.exitCode = failed.length === 0 ? 0 : 1
@@ -0,0 +1,99 @@
1
+ # 元素清单与用例矩阵 / Inventory and Case Matrix
2
+
3
+ ---
4
+
5
+ ## 一、元素清单 / Element inventory
6
+
7
+ 来源只有一个:**截图上的像素**。/ One source only: the pixels in a screenshot.
8
+
9
+ | 字段 Field | 说明 Notes |
10
+ | --- | --- |
11
+ | ID | `E-01` 顺序编号 |
12
+ | 界面 Surface | 元素所在的界面/窗口/页面 |
13
+ | 元素 / 工具 Element / tool | 用画面上的名字(按钮上的文字、图标含义) |
14
+ | 类型 Type | 按钮/输入框/菜单/工具栏/列表/表格/滚动区/弹窗/开关/选择器/状态提示/画布工具/导航 |
15
+ | 期望状态 Expected states | 默认·悬停·按下·选中·禁用·加载·错误·空(缺一不可) |
16
+ | 是否常用 Common? | 决定优先级 |
17
+ | 证据 Screenshot | 该元素所在的截图文件名 |
18
+
19
+ 英文对照表头 / English header:
20
+
21
+ ```text
22
+ ID | Surface | Element / tool | Type | Expected states | Priority | Evidence
23
+ ```
24
+
25
+ **反例 / Anti-pattern**:清单里出现"`onSubmit()` 函数"、"`/api/export` 接口"、"`users` 表"——
26
+ 这些不是画面元素,说明你去看代码了。删掉。
27
+
28
+ ---
29
+
30
+ ## 二、用例模板 / Case template
31
+
32
+ ```text
33
+ 用例 ID:C-012
34
+ 标题 / Title:导出当前画布为 PNG
35
+ 元素 / Element:E-07 工具栏「导出」按钮
36
+ 平台 / Platform:macOS 14.5 · 1920x1080 · 100% · 浅色
37
+ 前置 / Preconditions:已打开示例工程 sample-01;无未保存修改
38
+ 模式 / Mode:L1 仅鼠标
39
+ 步骤 / Steps:
40
+ 1. 移动鼠标到工具栏「导出」按钮,悬停 1 秒,观察是否出现提示
41
+ 2. 单击
42
+ 3. 等待导出面板出现(最多 5 秒)
43
+ 期望(画面)/ Expected (on screen):
44
+ - 出现模态面板,标题为「导出」;默认选中 PNG;有取消与确认按钮
45
+ - 悬停时按钮有可见的悬停态;点击时有按下态反馈
46
+ 证据 / Evidence:前后各一张截图 + 录屏 mm:ss
47
+ 结果 / Result:通过 / 失败 / 未验证
48
+ ```
49
+
50
+ **期望结果的写法 / How to write expectations**:写成"画面上能看见什么"。
51
+ "数据写入成功"不合格;"列表首行出现名为 sample-01 的条目,且状态列显示『已同步』"才合格。
52
+
53
+ ---
54
+
55
+ ## 三、矩阵 / The matrix
56
+
57
+ ```text
58
+ C-012|E-07 导出按钮|L1|P0|macOS
59
+ C-012|E-07 导出按钮|L2|P1|macOS
60
+ C-012|E-07 导出按钮|L3|P1|macOS
61
+ C-020|E-11 同步开关|L1|P0|iPhone
62
+ C-020|E-11 同步开关|L2|P2|iPhone(需外接键盘,若无则记"不适用")
63
+ ```
64
+
65
+ 裁剪规则 / Trimming: 不常用的元素(P3)在 L2/L3 可以只跑一次基线;
66
+ 但 **P0/P1 必须三级全跑**,因为它们的跨模式差异最有价值。
67
+ P0/P1 must run at all three modes; their cross-mode difference is the highest-value output.
68
+
69
+ ---
70
+
71
+ ## 四、优先级 / Priority
72
+
73
+ | 级别 | 含义 | 举例 |
74
+ | --- | --- | --- |
75
+ | P0 | 主流程,跑不通软件就没法用 | 打开工程、保存、导出、登录 |
76
+ | P1 | 常用功能与数据正确性 | 列表增删改、搜索、同步、设置生效 |
77
+ | P2 | 边缘与异常 | 空输入、超长文本、无权限、离线、快速重复点击、窗口缩放、iPad 旋转 |
78
+ | P3 | 打磨与一致性 | 动效、间距、深色模式配色、文案、跨端一致性 |
79
+
80
+ ---
81
+
82
+ ## 五、覆盖率怎么算 / Coverage arithmetic
83
+
84
+ ```text
85
+ 元素覆盖率 = 已执行用例覆盖的元素数 / 清单元素总数
86
+ 模式覆盖率 = 每元素实际执行的模式数 / 计划模式数
87
+ 平台覆盖率 = 实际测过的平台数 / 计划平台数
88
+ 用例执行率 = 已执行 / 计划
89
+ ```
90
+
91
+ 报告里必须给出这四个数,并列出未覆盖项与原因。/ Report all four, with uncovered items and reasons.
92
+
93
+ ---
94
+
95
+ ## 六、抽样策略(用例太多时)/ Sampling when the matrix explodes
96
+
97
+ 1. 先跑**每类元素各一个代表**的冒烟集(10–20 条),找出会崩的地方。
98
+ 2. P0 全量,P1 抽样 50%,P2 每类至少 1 条,P3 只跑视觉基线。
99
+ 3. 抽样规则必须写在计划里,事后不能改(改了就是选择偏差)。
@@ -0,0 +1,109 @@
1
+ # 权限清单与授予路径 / Permissions and Where to Grant Them
2
+
3
+ > 本方法只需要**捕获(看)**权限,**永远不需要控制(动)权限**。
4
+ > This method needs **capture (read)** permission and **never** control (inject) permission.
5
+ >
6
+ > 如果某个工具要求"辅助功能/输入监控/自动化"权限才能工作,那它超出了本方法的需要,不要授予。
7
+ > If a tool asks for accessibility, input-monitoring or automation permission, it exceeds what this
8
+ > method needs. Do not grant it.
9
+
10
+ ---
11
+
12
+ ## 一、需要向用户确认的权限 / Permissions to confirm with the user
13
+
14
+ | 权限 Permission | 必需 Required | 用途 Purpose | 被拒时 If denied |
15
+ | --- | --- | --- | --- |
16
+ | 鼠标操作 Mouse | 必需 | 一切界面操作 | 无法测试 |
17
+ | 键盘操作 Keyboard | 按级别 | L2/L3 的输入与导航 | 只跑 L1,L2/L3 记"未覆盖" |
18
+ | 屏幕录制 Screen recording | 与截屏二选一 | 过程、时序、动效、误触发 | 改用逐步截屏,报告注明"无连续录屏" |
19
+ | 截屏 Screenshot | 与录屏二选一 | 单点状态与元素细节 | 无法取证 → 停止测试 |
20
+ | 麦克风 Microphone | 建议 | 口述操作、对齐录屏时间点 | 改由操作者手写时间点 |
21
+ | 数据边界 Data boundary | 必需 | 避免把隐私录进证据 | 无法保证合规 → 缩小范围或停止 |
22
+
23
+ ---
24
+
25
+ ## 二、macOS
26
+
27
+ 1. **系统设置 → 隐私与安全性 → 屏幕录制**:勾选运行 Harness / 终端 / 录屏工具的应用。
28
+ 授予后**需要重启该应用**才生效。
29
+ 2. **麦克风**:系统设置 → 隐私与安全性 → 麦克风(要口述解说时)。
30
+ 3. **不需要**:辅助功能(Accessibility)、输入监控(Input Monitoring)、自动化(Automation)。
31
+ 如果你发现被测机器上这些权限给了非必要应用,收回它们。
32
+ 4. 光标:`screencapture -v` 录屏默认包含光标;QuickTime 录屏也含。若不含,必须用截屏补记指针位置。
33
+ 5. 试拍验证:
34
+
35
+ ```bash
36
+ node scripts/capture.mjs check
37
+ node scripts/capture.mjs shot "<session>" --label permission-probe
38
+ ```
39
+
40
+ 首次执行会触发系统权限提示;若返回黑屏或纯桌面图,说明权限未授予或未重启应用。
41
+
42
+ ---
43
+
44
+ ## 三、Windows
45
+
46
+ 1. **设置 → 隐私和安全性 → 屏幕截图和录制**:允许应用访问屏幕。
47
+ 2. **游戏栏 / Xbox Game Bar**:设置 → 游戏 → Xbox Game Bar 打开;`Win+G` 由**人手动**触发录制。
48
+ 3. **麦克风**:设置 → 隐私和安全性 → 麦克风。
49
+ 4. **不需要**:UI Automation 控制、`SendInput` 类自动化权限;不要安装/运行自动化框架。
50
+ 5. 只读截屏验证:`node scripts/capture.mjs shot "<session>" --label permission-probe`
51
+ (走 PowerShell `System.Drawing` 只读截屏,不注入输入)。
52
+ 6. 若桌面是远程会话(RDP),画面与真机不同,必须在报告里注明。
53
+
54
+ ---
55
+
56
+ ## 四、iPhone / iPad(iOS / iPadOS)
57
+
58
+ 1. **真机指纹/密码解锁**,并在设备上信任用于录制的电脑(如用 QuickTime 连续互通录制)。
59
+ 2. **控制中心 → 屏幕录制**:按住录制按钮可选择是否开启麦克风;开始前确认要录的是整块屏。
60
+ 3. **模拟器**:`xcrun simctl io booted screenshot` / `recordVideo` 需要 Xcode 命令行工具,不需要额外授权。
61
+ 4. **不必开**:开发者模式(仅当测试你自己的构建、需要安装包时才用,且要写进报告)。
62
+ 5. 人用手操作真机;模拟器上人用**真实鼠标**点击模拟器窗口——这不是注入。
63
+ 6. 注意 DRM 内容在录制时可能黑屏,这不是缺陷,报告里注明。
64
+ 7. 横竖屏、分屏、动态字体大小属于环境变量,测试前固定并记录。
65
+
66
+ ---
67
+
68
+ ## 五、Android 与其他设备 / Android and others
69
+
70
+ 1. `adb exec-out screencap -p`(截屏)、`adb shell screenrecord`(录屏)为只读捕获,允许。
71
+ 2. **禁止** `adb shell input ...` 与任何自动化驱动(见 `observed-ui-test` 技能的 BANNED-INPUTS.md)。
72
+ 3. 电视/车机/手表:用厂商投屏或采集卡做只读录制,遥控器与触摸由人操作。
73
+
74
+ ---
75
+
76
+ ## 六、英文问卷 / The English questionnaire
77
+
78
+ ```text
79
+ Before the observed UI test starts, please confirm:
80
+
81
+ [Input]
82
+ 1. Mouse: may I base findings on your real mouse actions? (yes / no)
83
+ 2. Keyboard: allowed, and up to which mode?
84
+ A. Mouse only (L1)
85
+ B. Mouse + keyboard, shortcuts forbidden (L2)
86
+ C. Mouse + keyboard + shortcuts (L3)
87
+
88
+ [Capture]
89
+ 3. Screen recording: allowed? Include the cursor? (yes / no; with / without cursor)
90
+ 4. Screenshots: allowed as evidence at any time? (yes / no)
91
+ 5. Microphone: allowed for spoken narration to line up the recording timeline? (yes / no)
92
+ 6. OS capture permission:
93
+ macOS: System Settings -> Privacy & Security -> Screen Recording (restart the app after granting)
94
+ Windows: Settings -> Privacy & security -> Screenshots and recording / Game Bar
95
+ iPhone/iPad: Control Centre screen recording (press and hold for the microphone)
96
+ Which will you grant?
97
+
98
+ [Scope]
99
+ 7. App under test: name, version, platform, devices (macOS / Windows / iPhone / iPad / other)
100
+ 8. Scope: which features are in, which are explicitly out
101
+ 9. Data boundary: which account/data; what must never appear in a recording (names, phone numbers,
102
+ keys, production data)
103
+ 10. Exit criteria: what makes us stop immediately (crash, data loss, privacy leak, production damage)
104
+
105
+ [Compliance]
106
+ This test uses no internal pointer directives: no injected pointer, touch or key events, no UI
107
+ automation drivers, no calls into the software's internals. Input comes only from your real
108
+ peripherals; evidence comes only from screen recording and screenshots. Do you confirm? (yes / no)
109
+ ```
@@ -0,0 +1,93 @@
1
+ # 观察式界面测试计划 / Observed UI Test Plan
2
+
3
+ > 计划阶段产物,中英对照。测试目录里的 `matrix.md` / `elements.md` 由脚手架生成。
4
+ > Phase-one artifact, bilingual. The session directory's `matrix.md` / `elements.md` come from the
5
+ > scaffold.
6
+
7
+ ---
8
+
9
+ ## 1. 目标 / Objective
10
+
11
+ 本次要回答的问题(写成可判定的问题)/ The questions this run must answer:
12
+
13
+ 1.
14
+ 2.
15
+ 3.
16
+
17
+ 不回答的问题(明确排除)/ Explicitly out of scope:
18
+
19
+ -
20
+
21
+ ## 2. 被测对象 / System under test
22
+
23
+ | 项目 Item | 内容 Value |
24
+ | --- | --- |
25
+ | 应用 App | |
26
+ | 版本 / 构建 Version / Build | |
27
+ | 平台 Platform | macOS / Windows / iPhone / iPad / other |
28
+ | 设备 Device | 型号 + 系统版本 |
29
+ | 安装方式 Install source | 应用商店 / 安装包 / 开发构建 / 模拟器 |
30
+ | 关键依赖 Dependencies | 网络、账号、外部设备 |
31
+
32
+ ## 3. 测试环境 / Environment
33
+
34
+ 分辨率与缩放 · 深色/浅色 · 动态字体大小 · 语言与区域 · 网络状态(在线/弱网/离线) ·
35
+ 外接屏数量 · iPad 方向与分屏状态。
36
+
37
+ ## 4. 权限与合规 / Permissions and compliance
38
+
39
+ | 项目 Item | 值 Value |
40
+ | --- | --- |
41
+ | 鼠标 Mouse | yes / no |
42
+ | 键盘 Keyboard | L1 / L2 / L3 |
43
+ | 屏幕录制 Screen recording | yes / no(含光标 / 不含) |
44
+ | 截屏 Screenshot | yes / no |
45
+ | 麦克风 Microphone | yes / no |
46
+ | 数据边界 Data boundary | |
47
+ | 内部指针指令 Internal pointer directives | **不使用 / none** |
48
+ | 硬件宏或系统辅助 Hardware macro / OS assist | 无 / 有(说明) |
49
+
50
+ ## 5. 范围与优先级 / Scope and priority
51
+
52
+ P0:
53
+ P1:
54
+ P2:
55
+ P3(抽样):
56
+
57
+ ## 6. 排期 / Schedule
58
+
59
+ | 阶段 Phase | 内容 | 预计时长 |
60
+ | --- | --- | --- |
61
+ | 准备 Preparation | 权限、清单、矩阵 | |
62
+ | L1 | 仅鼠标全量 | |
63
+ | L2 | 无快捷键全量 | |
64
+ | L3 | 快捷键 + 双路径一致性 | |
65
+ | 汇总 Report | 判定、报告、复测建议 | |
66
+
67
+ ## 7. 退出与中止条件 / Exit and abort criteria
68
+
69
+ **立即中止 / Abort immediately**:崩溃且无法恢复、数据损坏、隐私内容进入画面、误改生产数据、
70
+ 设备过热或异常。
71
+
72
+ **提前结束 / End early**:S1 阻断导致主流程完全不可用(先出中间报告)。
73
+
74
+ ## 8. 角色与产出 / Roles and artifacts
75
+
76
+ | 角色 Role | 人 Person |
77
+ | --- | --- |
78
+ | 操作者 Operator | |
79
+ | 观察者 Observer | |
80
+ | 记录者 Recorder | |
81
+ | 裁决者 Adjudicator | |
82
+
83
+ 产出 / Artifacts:会话目录(`session.json`、`elements.md`、`matrix.md`、`findings.jsonl`、
84
+ `evidence/`)与最终 `report.md`。
85
+
86
+ ## 9. 风险 / Risks
87
+
88
+ | 风险 Risk | 影响 | 缓解 Mitigation |
89
+ | --- | --- | --- |
90
+ | 无录屏权限 | 过程类缺陷无法取证 | 逐步截屏 + 操作者口述时间点 |
91
+ | 只有一台设备 | 跨端一致性无法验证 | 标"未验证",列入下一轮 |
92
+ | 用例太多 | 跑不完 | 按抽样规则裁剪并写明 |
93
+ | 真实数据 | 隐私风险 | 用测试账号与演示数据 |
@@ -0,0 +1,135 @@
1
+ ---
2
+ name: observed-test-plan
3
+ description: 观察式界面测试的测试前准备:向用户发出鼠标、键盘、录屏、截屏与软硬件权限问卷,确定被测范围与三级模式,只看画面建立功能元素清单,排出用例矩阵与优先级,并用脚手架脚本生成会话目录。Observed UI testing, preparation phase: ask the user the mouse/keyboard/screen-recording/screenshot and OS-permission questionnaire, pin scope and the three input modes, build the on-screen element inventory, and lay out the case matrix, generating the session scaffold.
4
+ ---
5
+
6
+ # 测试前准备 / Test Preparation
7
+
8
+ 这是 `observed-ui-test` 的第一阶段:**在碰软件之前**把权限、范围、元素清单和用例矩阵定下来。
9
+ 没有这一步,后面的缺陷会无法复现、覆盖会说不清。
10
+
11
+ The phase before touching the app: pin permissions, scope, inventory and matrix. Skip it and the
12
+ later defects will not reproduce and coverage cannot be explained.
13
+
14
+ ---
15
+
16
+ ## 1. 权限问卷(先问,必须问全)/ The permission questionnaire (ask first, ask all)
17
+
18
+ 把下面整段发给用户,等他逐条回答后再动。/ Send the whole block, wait for answers:
19
+
20
+ ```text
21
+ 开始观察式界面测试前,需要你确认以下几项权限与边界:
22
+
23
+ 【输入权限】
24
+ 1. 鼠标:允许使用真实鼠标操作并据此判定吗?(yes / no)
25
+ 2. 键盘:允许使用键盘吗?到哪一级?
26
+ A. 仅鼠标(L1)
27
+ B. 鼠标 + 键盘,但不许使用快捷键(L2)
28
+ C. 鼠标 + 键盘 + 快捷键(L3)
29
+
30
+ 【取证权限】
31
+ 3. 屏幕录制:允许录屏吗?录屏里要不要包含光标?(yes / no;含光标 / 不含)
32
+ 4. 截屏:允许随时截屏作为证据吗?(yes / no)
33
+ 5. 麦克风:允许口述操作解说、方便定位录屏时间点吗?(yes / no)
34
+ 6. 系统捕获权限:
35
+ macOS:系统设置 → 隐私与安全性 → 屏幕录制(授予后需重启应用)
36
+ Windows:设置 → 隐私和安全性 → 屏幕截图和录制 / 游戏栏
37
+ iPhone/iPad:控制中心录屏(长按可开麦克风);如用模拟器则无需此步
38
+ 你愿意授予哪些?(逐项回答)
39
+
40
+ 【测试范围】
41
+ 7. 被测应用:名称、版本、平台、设备(macOS / Windows / iPhone / iPad / 其他)
42
+ 8. 本次范围:要测哪些功能;哪些明确不测
43
+ 9. 数据边界:用哪个账号/数据;哪些内容绝对不能录进画面(真实姓名、手机号、密钥、生产数据)
44
+ 10. 退出条件:出现什么情况立即停止(崩溃、数据损坏、隐私泄露、误删生产数据)
45
+
46
+ 【合规声明】
47
+ 本次测试不会使用任何内部指针指令:不注入鼠标/触摸/按键事件,不驱动自动化框架,
48
+ 不调用软件内部句柄。输入只来自你的真实外设;证据只来自录屏与截屏。
49
+ 你是否确认这一点?(yes / no)
50
+ ```
51
+
52
+ English version of the same questionnaire is in [PERMISSIONS.md](PERMISSIONS.md);平台权限的具体授予
53
+ 路径与"没权限时怎么办"也在那里。
54
+
55
+ Rules:
56
+ - `screenRecording` 与 `screenshot` **至少一个为 yes**,否则无法进行观察式测试,直接停下并说明。
57
+ - 任何一项回答"待定"→ 不要开始,先把待定项变成明确答复。
58
+ - 拒绝的项记录在案,不允许绕过、不允许"偷偷用一下"。
59
+
60
+ ---
61
+
62
+ ## 2. 范围六项 / The six scope fields
63
+
64
+ 被测应用名+版本+构建号 · 平台与设备 · 输入设备 · 显示环境(分辨率/缩放/深浅色/动态字体) ·
65
+ 数据边界 · 退出条件。写进 `session.json` 的 `scope`,见 [PLAN-TEMPLATE.md](PLAN-TEMPLATE.md)。
66
+
67
+ ---
68
+
69
+ ## 3. 只看画面建元素清单 / Element inventory from the screen only
70
+
71
+ 1. 打开被测软件,把每个界面截图(用 `scripts/capture.mjs shot`)。
72
+ 2. 对着截图列出**看得见**的功能元素与工具:按钮、输入框、菜单、工具栏、列表、弹窗、
73
+ 开关、选择器、状态提示、画布与工具、导航。
74
+ 3. 每个元素写全期望状态:默认 / 悬停 / 按下 / 选中 / 禁用 / 加载 / 错误 / 空。
75
+ 4. **不许**打开代码、DOM 检查器、日志或数据库来补全清单;那些是线索不是清单来源。
76
+
77
+ 产出一张 `elements.md`(脚手架已生成表头)。
78
+
79
+ ---
80
+
81
+ ## 4. 用例矩阵与优先级 / Case matrix and priority
82
+
83
+ 矩阵 = 元素 × 平台 × 模式(L1/L2/L3)。规则与样例见 [MATRIX.md](MATRIX.md)。
84
+ 每条用例的"期望结果"必须是**画面上能看到的东西**。优先级 P0 主流程 → P1 常用与数据 → P2 边缘异常 →
85
+ P3 打磨一致性;P2 至少覆盖空输入、超长输入、无权限、离线、快速重复点击、窗口缩放/旋转。
86
+
87
+ ---
88
+
89
+ ## 5. 生成会话脚手架 / Generate the session scaffold
90
+
91
+ ```bash
92
+ node <skill-directory>/../scripts/session.mjs init ./ui-test-<app>-<date> \
93
+ --platform macos --app "<应用名>" --levels L1,L2,L3 --tester "<操作者>"
94
+ ```
95
+
96
+ 生成 / Creates:
97
+
98
+ ```text
99
+ ui-test-<app>-<date>/
100
+ ├── session.json # 闸门、范围、平台、级别、权限记录
101
+ ├── permissions.md # 权限问卷与答复存根(中英对照)
102
+ ├── elements.md # 元素清单表头
103
+ ├── matrix.md # 用例矩阵表头
104
+ ├── findings.jsonl # 一条缺陷一行
105
+ ├── report.md # 报告初稿
106
+ └── evidence/ # 截图与录屏
107
+ ```
108
+
109
+ 记录权限答复 / Record the answers:
110
+
111
+ ```bash
112
+ node <skill-directory>/../scripts/session.mjs gate ./ui-test-<app>-<date> \
113
+ --mouse yes --keyboard L2 --screen-recording yes --screenshot yes \
114
+ --microphone no --os-permission yes --cursor yes --data-boundary "仅测试账号"
115
+ ```
116
+
117
+ 闸门未通过时不要开始测试;`status` 会告诉你还缺什么:
118
+ `node <skill-directory>/../scripts/session.mjs status ./ui-test-<app>-<date>`。
119
+
120
+ ---
121
+
122
+ ## 6. 准备完成检查表 / Readiness checklist
123
+
124
+ - [ ] 权限问卷逐条有明确答复,并已落盘
125
+ - [ ] 至少一种取证方式可用(录屏或截屏),且已实际试拍成功
126
+ - [ ] 范围六项写全,退出条件写清
127
+ - [ ] 元素清单来自截图,不是来自代码
128
+ - [ ] 每个元素的期望状态列全
129
+ - [ ] 用例矩阵覆盖 P0 与 P2 的最低要求
130
+ - [ ] 被测机器上自动化工具、宏软件全部退出;辅助功能权限未授予任何非必要应用
131
+ - [ ] 已跑 `guard.mjs scan <session>`,注入线索已落盘(只读观察,不执行注入)
132
+ - [ ] 操作者与观察者分工确认(谁操作、谁读图、谁记录)
133
+
134
+ 全部打勾后,切到 `observed-ui-test` 技能开始按 L1 → L2 → L3 执行。
135
+ When every box is ticked, switch to the `observed-ui-test` skill and run L1 → L2 → L3.