diff --git a/HANDOFF.md b/HANDOFF.md index c150bf8..6a51925 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -11,7 +11,7 @@ | 设备 | 已装包与执行 SHA | 数据 / 输入源 | 恢复点 | |---|---|---|---| -| **air**(arm64 / macOS 27.2 / Xcode 27,用户 `joker`;pi 跑在 Background 会话,驱动不了 GUI 点击) | **1.2.0 / build 39**(候选,2026-11-07 装机),宿主执行 `b152a001…`;输入源 `Lampyris.default` 在用;回滚点 `updates/install-backup-20261107T063934Z/` | config `5d4271fb…`(`full_width=half` 已是 ADR-305 枚举形态)、词库 1139 行(用户在用,行数会变) | `uninstalled/`、`updates/` 可回滚到 1.0.0…1.0.2 | +| **air**(arm64 / macOS 27.2 / Xcode 27,用户 `joker`;pi 跑在 Background 会话,驱动不了 GUI 点击) | **1.2.0 / build 39(B4+B5 候选,2026-11-07 装机)**,宿主执行 `28e24d26…`;输入源 `Lampyris.default` 在用;`show_wubi_learning=false` | config `5d4271fb…`(`full_width=half` 已是 ADR-305 枚举形态)、词库 1139 行(用户在用,行数会变) | `uninstalled/`、`updates/` 可回滚到 1.0.0…1.0.2 | | **pro**(Intel / macOS 13.7.8,用户 `bot`,SSH 别名 `pro`) | **1.1.0 / build 38**,宿主执行 `d3c5a2ef…`(2026-11-06 经更新程序升级,旧版备份在 `updates/upgrade-2026-11-06T14-27-15Z/backup/`) | config 与词库保持 1.0.0 基线(`show_wubi_learning=true` 是用户有意设置) | 升级前有 `uninstalled/bundle.*` | ## 2. 本轮完成 @@ -61,6 +61,7 @@ - **B3 学习区已实现(报告 253)**:⌥L 学习区两行式(完整码+简码 / 字根格+识别码),候选窗零改动;词组按输入码对应到字;`LearningHints` 读 `B0-runtime.tsv`,缺失退回 D14。离屏实拍 `docs/design/learning-real-*.png`。**装机后的性能/内存实测仍待候选包**。 - **B3 草图(已成历史)**(`docs/design/l3-hint.md` + `learning-area-mock.png`):按用户裁定改为**候选窗零改动**,学习信息只放 ⌥L 学习区,学习区自己两行封顶(单字:字+码+简码 / 字根+识别码依据;词组:前 3 字 + 「逐字 ▸」)。`partial` 未知字根显示 `?`、标注「拆解待核验」;数据源优先 B0-runtime,未覆盖的字走 D14 兜底。**实现未开始,等评审三个细节**。 - **B5 已收口(ADR-314 / 报告 258)**:冷首键 ~28 ms(学习区关着也一样,用户不可感知)→ 不优化;A1/A2/A3/A5 全部关闭并写触发条件;内存按 ADR-312 双阈值(默认 30 / 学习区 34)。 +- **练习窗口版式已修正(报告 260)**:翻转坐标从顶部排、固定 460×336、题干居中、进度行不再被裁、A/C 共用纵向基准,并加了版式自检(控件不越界、不与题型控件重叠)。**待装包看真机**。 - **B4 练习已实现(ADR-313 / 报告 257)**:只看 `full` 出题,题型 A(看字打码)+ C(识别码判断);进度写 `~/Library/Application Support/Lampyris/practice.tsv`(有界 3,000 行 / 256 KiB、原子写、删除即重置);快捷键 `hotkey.open_practice` 默认 **⌥K**,入口在设置页快捷键列表;**尚未装包实测**。 - **B2 数据地基已出产物(报告 252 / ADR-311)**:`data/generated/B0-runtime.tsv`(3,000 字、287,310 B、96 B/字,预算 512 KiB 有余量),已登记 derived `b0-runtime` 并有体积/契约测试;**宿主装载与 L3 提示属 B3,尚未做**。 @@ -124,6 +125,7 @@ make macos-offline-tests ## 9. 会话日志(最近) +- 2026-11-06 / pi(air):**1.2.0(B4+B5)候选装机(报告 259)**。PR #232 → CI 包(`exec 28e24d26…`)装 air;稳态 p95 **5.80 ms**(三段 5.12–5.97)、冷首键 28.86/33.39、0 外联、memory-gate 私有 **28.5–28.8 MiB**(限 30)PASS。待用户走查:⌥K 练习窗口(A/C 两种题型)、⌥L 学习区默认关、practice.tsv 生成与重置。 - 2026-11-06 / pi(air):**1.2.0/build 39 装机 + 首轮体验数据(报告 254)**。PR #227 合并 → CI 验收包 → 装 air:版本/执行 SHA `b152a001…`/公证全部核对,config 与 1225 行词库未变。评测 ALL 命中 98.2%/首选 86.4%(jianma2 100%/100%);三段延迟稳态 p95 6.7–8.3 ms;**冷首键 27–40 ms,比 1.1.0 的 16–26 ms 高**(待查是否学习区布局所致);soak 3 分钟 17 轮 0 外联、footprint 38.5 MiB(比 1.1.0 高约 5 MiB,待单独归因)。 - 2026-11-06 / pi(air):**B3 学习区实现(报告 253)**。`WubiLearningView` 改两行布局(完整码+简码 / 字根格+识别码两行),候选窗零改动;新增 `LearningHints`(读 `B0-runtime.tsv`,缺失退回 D14);词组按输入码位置映射到字(KWWL → 中K 华W 人W 国L)。`wubi-learning.swift` 扩到可注入资源并生成亮/暗实拍;离线回归全绿。 - 2026-11-06 / pi(air):**B2 数据地基(报告 252 / ADR-311)**。B0 扩到 3,000 字并预渲染成 `B0-runtime.tsv`(287,310 B ≈ 96 B/字,预算 512 KiB 反而有余量;B0.json 同规模是 1.62 MiB),登记 derived `b0-runtime`,新增 `test-b0-runtime.py` 守体积/格式/无 PUA/消费契约。ADR-311 定装载方式(bundle 内只读文件、宿主查表、Rust 不参与)。 diff --git a/docs/design/practice-mock.png b/docs/design/practice-mock.png new file mode 100644 index 0000000..f7e26f1 Binary files /dev/null and b/docs/design/practice-mock.png differ diff --git a/docs/reviews/259-1.2.0-b4b5-candidate.md b/docs/reviews/259-1.2.0-b4b5-candidate.md new file mode 100644 index 0000000..b667f9c --- /dev/null +++ b/docs/reviews/259-1.2.0-b4b5-candidate.md @@ -0,0 +1,46 @@ +# 259 — 1.2.0(B4+B5)候选装机与机器验收数据 + +- 日期:2026-11-06 | 授权:用户「装」 | 环境:air(arm64 / macOS 27.2) +- 候选:main `99e9b15…`(PR #232)→ CI 验收包 + +## 1. 包身份 + +| 项 | 值 | +|---|---| +| ZIP SHA | `4039cdfcabd03e55…` | +| 宿主执行 SHA | `28e24d26519f83a4…`(装前包内 = 装后已装) | +| 版本 / 构建 | 1.2.0 / 39 | +| 签名 | Notarized Developer ID | +| 回滚点 | `updates/install-backup-*`(上一候选) | +| config | 未变(`show_wubi_learning=false` 默认;`hotkey.open_practice` 尚无显式行 → 用 Schema 默认 `alt+k`) | + +## 2. 机器验收(装上后实测) + +| 项 | 结果 | +|---|---| +| 稳态 p95(三段) | **5.80 ms**(basic 5.97 / long-code 5.26 / paging 5.12)PASS | +| 冷首键 | p50 28.86 / p95 33.39(与 B5 结论一致) | +| 外联 | **0** 连接 | +| 内存(memory-gate 3 轮) | 私有已提交 **28.5 / 28.6 / 28.7–28.8** MiB,限 30 → **PASS** | +| footprint(perf 单点) | 32.2 MiB(参考值) | + +## 3. 待用户人工走查(本轮重点) + +1. **⌥K 打开练习窗口**: + - 题型 A(看字打码):随便打几个,确认对错反馈与"显示答案"; + - 题型 C(识别码判断):选末笔 + 结构,确认判分与解释; + - 关窗后再按 ⌥K,确认能重新打开且不残留多个窗口。 +2. **⌥L 学习区**:默认关闭;在 TextEdit 里打开,确认两行式(完整码+简码 / 字根格+识别码)。 +3. **练习进度**:`~/Library/Application Support/Lampyris/practice.tsv` 应出现并随答题增长; + 「打开进度文件」按钮应定位到该文件;删掉文件即重置。 +4. **热键不冲突**:⌥K 在 TextEdit / Terminal 里都不应插入字符(Terminal 的 ⌥L 冲突是终端自身行为,⌥K 未见于其快捷键表)。 + +## 4. 已知限制(本轮不修) + +- 覆盖安装会**让 macOS 把输入源移出启用列表**(报告 255 §6),本次装后已确认 `current` 仍为萤火虫; + 若菜单里消失,按 `docs/故障恢复.md` §4.1 重新添加。 +- 练习窗口尚未做深色模式/无障碍专项走查(题面用系统颜色,未自定义)。 + +## 5. 关联 + +ADR-313/314、报告 257/258、`docs/design/l3-hint.md`。 diff --git a/docs/reviews/260-practice-ui-layout.md b/docs/reviews/260-practice-ui-layout.md new file mode 100644 index 0000000..25a8bbb --- /dev/null +++ b/docs/reviews/260-practice-ui-layout.md @@ -0,0 +1,34 @@ +# 260 — 练习窗口 UI 修正:不再压字、不越界、两类题型共用一套版式 + +- 日期:2026-11-06 | 来源:用户截图反馈「练习的 UI 界面需要优化调整」 +- 现象(截图):题干汉字**压在题型切换控件上**;中部大片空白;底部「已练 N 字」被窗口边缘裁掉。 + +## 1. 根因 + +旧版用手写 frame 且**从窗口底部往上算**(非翻转坐标),高度估算不准就会: +题干与顶部控件重叠、进度行落到窗口之外。题型 C 的两行按钮还额外占用纵向空间,更容易冲突。 + +## 2. 改法 + +- 内容容器改为**翻转坐标**(`isFlipped = true`),一律**从顶部往下**排: + 题型切换 → 题干 → 提示 → 作答区 → 反馈 → 按钮 → 进度; +- 固定窗口尺寸 **460 × 336**,各行显式坐标(A 与 C 共用同一套纵向基准): + 20 / 56 / 120 / 144–210 / 218 / 264 / 300; +- 题干 40pt **居中**;提示与反馈居中;输入框整宽; +- 题型 C 增加「末笔」「结构」两个小标题,选中后标题就地更新为「末笔 撇」; +- 底部按钮等距(120×28,间距 10),进度行固定在最下方、不再越界; +- 新增**版式自检**(`layoutProblemsForTesting`):A/C 两种题型下,所有可见控件必须落在内容区内, + 且不得与顶部题型控件重叠——离线回归里断言为空。 + +## 3. 视觉 + +`docs/design/practice-mock.png`(按新坐标绘制,A 题型示例)。 + +## 4. 验证 + +- `make verify` PASS;离线回归 **62 文件 / 328 例 PASS**(含新增版式自检)。 +- 真机确认待装包(下一次候选)。 + +## 5. 关联 + +ADR-313、报告 257/259。 diff --git a/platforms/macos/Sources/PracticeWindow.swift b/platforms/macos/Sources/PracticeWindow.swift index 0e6057f..be74de1 100644 --- a/platforms/macos/Sources/PracticeWindow.swift +++ b/platforms/macos/Sources/PracticeWindow.swift @@ -1,32 +1,50 @@ import AppKit /// 练习窗口(ADR-313):独立、非模态、可随时关闭;不碰候选窗,不常驻焦点。 +/// +/// 布局用**翻转坐标**从顶部往下排,避免手动从底部算高度导致文字出界(报告 260): +/// 题型切换 → 题干 → 提示 → 作答区 → 反馈 → 按钮 → 进度。 final class PracticeWindow: NSObject, NSWindowDelegate { static let shared = PracticeWindow() + static let contentSize = NSSize(width: 460, height: 336) private var window: NSWindow? + private var content: FlippedView? private let questionLabel = NSTextField(labelWithString: "") - private let detailLabel = NSTextField(labelWithString: "") - private let feedbackLabel = NSTextField(labelWithString: "") + private let promptLabel = NSTextField(labelWithString: "") + private let feedbackLabel = NSTextField(wrappingLabelWithString: "") private let progressLabel = NSTextField(labelWithString: "") private let answerField = NSTextField(string: "") private let kindControl = NSSegmentedControl(labels: PracticeKind.allCases.map(\.title), trackingMode: .selectOne, target: nil, action: nil) + private let strokeTitle = NSTextField(labelWithString: "末笔") + private let structureTitle = NSTextField(labelWithString: "结构") private let strokeButtons = Practice.strokes.map { NSButton(title: $0, target: nil, action: nil) } private let structureButtons = Practice.structures.map { NSButton(title: $0, target: nil, action: nil) } - private let progress = PracticeProgress(url: PracticeProgress.defaultURL()) + private var practiceProgress: PracticeProgress? private var bank: [PracticeQuestion] = [] private var current: PracticeQuestion? private var pendingStroke: String? private var pendingStructure: String? + private var lastAnswer: String? + + private var progress: PracticeProgress { + if let practiceProgress { return practiceProgress } + let created = PracticeProgress(url: PracticeProgress.defaultURL()) + practiceProgress = created + return created + } private var kind: PracticeKind { PracticeKind.allCases[max(0, min(PracticeKind.allCases.count - 1, kindControl.selectedSegment))] } + // MARK: 展示 + func show() { if window == nil { build() } kindControl.selectedSegment = 0 + applyKindVisibility() reload() window?.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) @@ -34,87 +52,101 @@ final class PracticeWindow: NSObject, NSWindowDelegate { } private func build() { - let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 420, height: 300), + let window = NSWindow(contentRect: NSRect(origin: .zero, size: Self.contentSize), styleMask: [.titled, .closable], backing: .buffered, defer: false) window.title = "萤火虫 · 练习" window.isReleasedWhenClosed = false window.delegate = self + window.contentView = buildContent() window.center() + self.window = window + } - let container = NSView() - var y: CGFloat = 250 - func place(_ view: NSView, height: CGFloat, width: CGFloat = 360) { - view.frame = NSRect(x: 30, y: y, width: width, height: height) - container.addSubview(view) - y -= height + 10 - } + private func buildContent() -> NSView { + let content = FlippedView(frame: NSRect(origin: .zero, size: Self.contentSize)) + let width = Self.contentSize.width + let margin: CGFloat = 24 + kindControl.frame = NSRect(x: margin, y: 20, width: width - margin * 2, height: 26) kindControl.target = self kindControl.action = #selector(kindChanged) - place(kindControl, height: 26) + content.addSubview(kindControl) - questionLabel.font = .systemFont(ofSize: 34, weight: .medium) - place(questionLabel, height: 44) + questionLabel.alignment = .center + questionLabel.font = .systemFont(ofSize: 40, weight: .medium) + questionLabel.frame = NSRect(x: margin, y: 56, width: width - margin * 2, height: 60) + content.addSubview(questionLabel) - detailLabel.font = .systemFont(ofSize: 12) - detailLabel.textColor = .secondaryLabelColor - place(detailLabel, height: 18) + promptLabel.alignment = .center + promptLabel.font = .systemFont(ofSize: 12) + promptLabel.textColor = .secondaryLabelColor + promptLabel.frame = NSRect(x: margin, y: 120, width: width - margin * 2, height: 18) + content.addSubview(promptLabel) + answerField.frame = NSRect(x: margin, y: 148, width: width - margin * 2, height: 26) answerField.placeholderString = "输入完整五笔码后回车" answerField.target = self answerField.action = #selector(submitCode) - place(answerField, height: 24) + answerField.font = .monospacedSystemFont(ofSize: 14, weight: .regular) + content.addSubview(answerField) + // 识别码:两行带标题的按钮组 + strokeTitle.font = .systemFont(ofSize: 11) + strokeTitle.textColor = .secondaryLabelColor + strokeTitle.frame = NSRect(x: margin, y: 148, width: 32, height: 24) + content.addSubview(strokeTitle) for (index, button) in strokeButtons.enumerated() { button.target = self button.action = #selector(pickStroke(_:)) button.tag = index button.bezelStyle = .rounded - button.frame = NSRect(x: 30 + CGFloat(index) * 70, y: y, width: 64, height: 26) - container.addSubview(button) + button.frame = NSRect(x: margin + 36 + CGFloat(index) * 74, y: 144, width: 68, height: 28) + content.addSubview(button) } - y -= 36 + structureTitle.font = .systemFont(ofSize: 11) + structureTitle.textColor = .secondaryLabelColor + structureTitle.frame = NSRect(x: margin, y: 186, width: 32, height: 24) + content.addSubview(structureTitle) for (index, button) in structureButtons.enumerated() { button.target = self button.action = #selector(pickStructure(_:)) button.tag = index button.bezelStyle = .rounded - button.frame = NSRect(x: 30 + CGFloat(index) * 100, y: y, width: 94, height: 26) - container.addSubview(button) + button.frame = NSRect(x: margin + 36 + CGFloat(index) * 108, y: 182, width: 100, height: 28) + content.addSubview(button) } - y -= 40 + feedbackLabel.alignment = .center feedbackLabel.font = .systemFont(ofSize: 12) - place(feedbackLabel, height: 18) - - progressLabel.font = .systemFont(ofSize: 11) - progressLabel.textColor = .secondaryLabelColor - place(progressLabel, height: 16) + feedbackLabel.frame = NSRect(x: margin, y: 218, width: width - margin * 2, height: 40) + content.addSubview(feedbackLabel) let next = NSButton(title: "下一题", target: self, action: #selector(nextQuestion)) - next.bezelStyle = .rounded - next.frame = NSRect(x: 30, y: 12, width: 90, height: 26) - container.addSubview(next) let reveal = NSButton(title: "显示答案", target: self, action: #selector(revealAnswer)) - reveal.bezelStyle = .rounded - reveal.frame = NSRect(x: 130, y: 12, width: 90, height: 26) - container.addSubview(reveal) let export = NSButton(title: "打开进度文件", target: self, action: #selector(openProgress)) - export.bezelStyle = .rounded - export.frame = NSRect(x: 230, y: 12, width: 110, height: 26) - container.addSubview(export) + for (index, button) in [next, reveal, export].enumerated() { + button.bezelStyle = .rounded + button.frame = NSRect(x: margin + CGFloat(index) * 130, y: 264, width: 120, height: 28) + content.addSubview(button) + } - window.contentView = container - self.window = window - applyKindVisibility() + progressLabel.font = .systemFont(ofSize: 11) + progressLabel.textColor = .secondaryLabelColor + progressLabel.frame = NSRect(x: margin, y: 300, width: width - margin * 2, height: 16) + content.addSubview(progressLabel) + return content } private func applyKindVisibility() { let isCode = kind == .code answerField.isHidden = !isCode + strokeTitle.isHidden = isCode + structureTitle.isHidden = isCode for button in strokeButtons + structureButtons { button.isHidden = isCode } } + // MARK: 交互 + @objc private func kindChanged() { applyKindVisibility() reload() @@ -129,49 +161,58 @@ final class PracticeWindow: NSObject, NSWindowDelegate { @objc private func nextQuestion() { guard !bank.isEmpty else { questionLabel.stringValue = "题库为空" - detailLabel.stringValue = "当前没有可出题的完整条目(需要 quality=full)" + promptLabel.stringValue = "当前没有可出题的完整条目(需要 quality=full)" return } current = bank.randomElement() + lastAnswer = nil answerField.stringValue = "" pendingStroke = nil pendingStructure = nil feedbackLabel.stringValue = "" guard let question = current else { return } questionLabel.stringValue = question.char - if kind == .code { - detailLabel.stringValue = "打出它的完整五笔码(\(question.code.count) 位)" - } else { - detailLabel.stringValue = "拆解:\(question.rootHint) · 选末笔与结构" - } + promptLabel.stringValue = kind == .code + ? "打出它的完整五笔码(\(question.code.count) 位)" + : "拆解:\(question.rootHint)" progressLabel.stringValue = progress.summary if kind == .code { window?.makeFirstResponder(answerField) } } @objc private func submitCode() { guard let question = current else { return } - let correct = Practice.judgeCode(answerField.stringValue, question: question) + let answer = answerField.stringValue + let correct = Practice.judgeCode(answer, question: question) + lastAnswer = answer finish(correct: correct, explanation: "完整码 \(question.code.uppercased()) · 字根 \(question.rootHint)") } @objc private func pickStroke(_ sender: NSButton) { pendingStroke = Practice.strokes[sender.tag] + updateSelectionTitle() evaluateRecognition() } @objc private func pickStructure(_ sender: NSButton) { pendingStructure = Practice.structures[sender.tag] + updateSelectionTitle() evaluateRecognition() } + private func updateSelectionTitle() { + if let stroke = pendingStroke { strokeTitle.stringValue = "末笔 \(stroke)" } + if let structure = pendingStructure { structureTitle.stringValue = "结构 \(structure)" } + } + private func evaluateRecognition() { guard let question = current, let stroke = pendingStroke, let structure = pendingStructure else { - feedbackLabel.stringValue = "选末笔与结构各一个" + feedbackLabel.stringValue = "选一个末笔和一个结构" + feedbackLabel.textColor = .secondaryLabelColor return } let correct = Practice.judgeRecognition(stroke: stroke, structure: structure, question: question) let letter = question.recognition?.letter ?? "—" - finish(correct: correct, explanation: "识别码 \(letter)(\(stroke)·\(structure))· 完整码 \(question.code.uppercased())") + finish(correct: correct, explanation: "识别码 \(letter)(\(stroke)·\(structure))") } private func finish(correct: Bool, explanation: String) { @@ -192,4 +233,56 @@ final class PracticeWindow: NSObject, NSWindowDelegate { progress.save() NSWorkspace.shared.activateFileViewerSelecting([progress.url]) } + + // MARK: 测试 + + /// 离屏渲染(测试用):建一个不显示的窗口承载视图(AppKit 控件需要 window 才能绘制)。 + func renderForTesting(kind: PracticeKind, progressURL: URL, light: Bool) -> NSBitmapImageRep? { + practiceProgress = PracticeProgress(url: progressURL) + let view = buildContent() + kindControl.selectedSegment = PracticeKind.allCases.firstIndex(of: kind) ?? 0 + applyKindVisibility() + bank = Practice.bank(kind: kind) + current = bank.first + if let question = current { + questionLabel.stringValue = question.char + promptLabel.stringValue = kind == .code + ? "打出它的完整五笔码(\(question.code.count) 位)" + : "拆解:\(question.rootHint)" + feedbackLabel.stringValue = kind == .code ? "✓ 正确 · 完整码 \(question.code.uppercased())" + : "选一个末笔和一个结构" + } + progressLabel.stringValue = progress.summary + let host = NSWindow(contentRect: NSRect(origin: .zero, size: Self.contentSize), + styleMask: [.titled], backing: .buffered, defer: false) + host.contentView = view + if let appearance = NSAppearance(named: light ? .aqua : .darkAqua) { host.appearance = appearance } + view.layoutSubtreeIfNeeded() + guard let rep = view.bitmapImageRepForCachingDisplay(in: view.bounds) else { return nil } + view.cacheDisplay(in: view.bounds, to: rep) + return rep + } + + /// 版式自检(测试用):所有控件必须落在内容区里,且不与顶部题型控件重叠。 + func layoutProblemsForTesting(kind: PracticeKind) -> [String] { + let view = buildContent() + kindControl.selectedSegment = PracticeKind.allCases.firstIndex(of: kind) ?? 0 + applyKindVisibility() + var problems: [String] = [] + let bounds = view.bounds + for sub in view.subviews where !sub.isHidden { + if !bounds.contains(sub.frame) { + problems.append("\(type(of: sub)) 越界 \(sub.frame)") + } + if sub !== kindControl, sub.frame.intersects(kindControl.frame.insetBy(dx: 0, dy: -4)) { + problems.append("\(type(of: sub)) 与题型控件重叠") + } + } + return problems + } +} + +/// 顶部起算的容器:文字与控件按"距顶部"排,避免出界。 +private final class FlippedView: NSView { + override var isFlipped: Bool { true } } diff --git a/platforms/macos/tests/wubi-learning.swift b/platforms/macos/tests/wubi-learning.swift index ef761bb..d92aa74 100644 --- a/platforms/macos/tests/wubi-learning.swift +++ b/platforms/macos/tests/wubi-learning.swift @@ -31,6 +31,11 @@ require(Practice.judgeCode(sample.code.uppercased() + " ", question: sample), " require(!Practice.judgeCode("zzzz", question: sample), "错误码判错") require(Practice.recognitionLetter(stroke: "横", structure: "左右") == "G", "识别码表 横/左右 → G") require(Practice.recognitionLetter(stroke: "折", structure: "杂合") == "V", "识别码表 折/杂合 → V") +// ADR-313 练习窗口:版式自检(控件不越界、不与题型控件重叠)。 +for kind in [PracticeKind.code, PracticeKind.recognition] { + let problems = PracticeWindow.shared.layoutProblemsForTesting(kind: kind) + require(problems.isEmpty, "练习窗口版式问题:\(problems.joined(separator: "; "))") +} let tmpProgress = PracticeProgress(url: URL(fileURLWithPath: NSTemporaryDirectory() + "lampyris-practice-test" + "/practice.tsv")) for index in 0..<3200 { tmpProgress.record(char: "字", kind: .code, correct: index % 2 == 0, now: index) } require(tmpProgress.rows.count <= PracticeProgress.maxRows, "进度行数有界")