Repository navigation
finding(fields): openNativePicker has zero test coverage — DateField is its sole router and no case fires a click #7570
Description
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 5, 2026 分诊 —
domain:ui/priority:p3/pm:queue/finding/tests锚定 (anchoring):
packages/fields⇒domain:ui。在
origin/maina472b07上复核 —— 零命中成立,对照发火(而且比卡片给的更强)openNativePicker 出现在测试文件中:0 CONTROL toDateInputValue: 2 个文件 packages/fields/src/standard-widgets.test.tsx packages/fields/src/widgets/nativeDateValue.test.ts⭐ 卡片的对照给的是 1 个文件(
nativeDateValue.test.ts),实测是 2 —— 对照比它自己说的还更有力,零命中因此更可信。唯一路由点也确认了:
packages/fields/src/widgets/DateField.tsx:6 import { openNativePicker } from './openNativePicker.js'; packages/fields/src/widgets/DateField.tsx:57 openNativePicker(e.currentTarget);定级理由
priority:p3—— 采纳卡片的自评并复核其分寸:⚠️ 卡片明确不主张这是活缺陷:"No wrong behaviour was observed — this is a recorded coverage absence, the same class as finding(fields): standard-widgets.test.tsx covers 7 of the 10 standard widgets it imports — DateField, PhoneField and TextAreaField have no case in the file named for that set #7336, and it is unquantified in the same way."- 覆盖缺口本身不是缺陷;但这一处的形状值得记:
DateField本身覆盖良好(datetime-widgets.test.tsx有专门的块,__tests__/date-locale-channel.test.tsx钉了 locale 通道),却没有一个用例点击过 —— 所以"这个文件覆盖得好"这句话对这一行不成立。⭐ 这正是覆盖率数字最容易骗人的形态。 - 不上 p2:无观察到的错误行为。
定型:测试补齐,不踩 manual floor
⛔ 给执行席的边界
-
⭐ 卡片指出了要钉的两件事,两件都要,⛔ 不要只做第一件:
- helper 是否被调用;
- 它是否吞掉了宿主经 DOM props 传入的
onClick。
⚠️ 第二件是真正有价值的那件:DateField.tsx:57-58的写法是openNativePicker(...)然后domProps.onClick?.(e),一个只断言"picker 被调用"的测试不会发现有人把这两行调换或短路。 -
⛔ 不要为了覆盖率去 mock
openNativePicker。那会让测试断言"我们调用了我们 mock 的那个东西",覆盖率上去了、保护为零。用真实 helper + 一个可观察的副作用(例如showPicker的 spy)。 -
⚠️ openNativePicker大概率会碰浏览器 API(showPicker()),而本仓的 DOM 环境是 happy-dom —— ⭐ 本轮已在 plugin-charts: a categorical x axis above 5 buckets still loses labels — minTickGap 48 is a time-series budget charged to band axes #7386 上记录过它报告零文本度量、导致"因错误的原因通过"。⛔ 动手前先确认 happy-dom 是否实现了这个 API;若没有,测试要么注入替身、要么诚实地标注"只钉调用路径不钉浏览器行为"。 -
与 finding(fields): standard-widgets.test.tsx covers 7 of the 10 standard widgets it imports — DateField, PhoneField and TextAreaField have no case in the file named for that set #7336 同族(同一位实现者、同一类"记录缺席"),⛔ 不要合卡 —— 不同文件、不同 helper。
⛔ 分诊席不认领、不派单、不写码。
Generated by Claude Code
Claim: PM 席(
domain:uilane)认领本卡并派发给os-dev执行席。- Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
- Branch:
claude/issue-7570-opennativepicker-coverage - Worktree:
/home/user/objectui-issue-7570 - Domain:
domain:ui - File surface:
packages/fields/src/widgets/下的测试文件(新增或就近扩写)。⛔ 非必要不改openNativePicker.ts/DateField.tsx本身。 - Container & model: 本会话容器 ·
opus - Clause-②: no
- Serial constraints cleared: 同 lane 在飞的四张卡落在
packages/plugin-charts/、packages/app-shell/src/views/metadata-admin/、packages/app-shell/src/views/studio-design/、packages/plugin-timeline/——与packages/fields/src/widgets/不相交。
PM 裁定
这是一张覆盖率缺口卡,不是 bug 卡。⛔ 不要为了"修点什么"去改
openNativePicker或DateField的行为——卡片明确写着 "Not claimed: that this is a live defect."交付物:卡片自己点名的两条,各一条测试
- 点击时 helper 确实被调用;
- 宿主经 DOM props 传入的
onClick不被吞掉(现有实现里domProps.onClick?.(e)就在 helper 调用之后,这条是把它钉住)。
⭐ 硬性要求:ablation(否则这两条测试证明不了任何东西)
覆盖率卡最容易交付的假货,是一条"跑过了但删掉被测代码依然绿"的测试。所以必须给出变异对照:
- 从
DateField.tsx的onClick里删掉openNativePicker(e.currentTarget);这一行 ⇒ 第 1 条测试必须变红并自报其名; - 删掉
domProps.onClick?.(e);⇒ 第 2 条必须变红。
⛔ 变异必须落盘后先自证已落盘(改前/改后的
git hash-object不同、锚点计数翻转),跑完用git checkout HEAD -- <绝对路径>在trap … EXIT INT TERM里还原,并以状态证明还原(git diff HEAD为空 + hash 等于 HEAD blob)。⛔ 不要以"trap 应该跑了"当作还原证据。⚠️ 如果测试写出来发现了真缺陷如实报回(
open_questions/out_of_scope_findings),⛔ 不要在本卡里顺手改行为——本卡的裁定面是"补上缺失的钉子",改行为是另一张卡的事。
Generated by Claude Code
os-dev-report
{ "issue": 7570, "status": "done", "branch": "claude/issue-7570-opennativepicker-coverage", "pr": "https://github.com/objectstack-ai/objectui/pull/7818", "premise_still_valid": true, "summary": "The card's premise re-verified on origin/main 900f8d995: `git grep -l 'openNativePicker' -- '*.test.ts' '*.test.tsx'` returned nothing, the control `toDateInputValue` returned two files, and `DateField.tsx` is still the sole call site (the onClick handler). Added ONE test file, `packages/fields/src/widgets/openNativePicker.test.tsx` (8 tests), plus an empty-frontmatter changeset; no behaviour was changed — `openNativePicker.ts` and `DateField.tsx` are byte-identical to origin/main on this branch. The two pins the card names are PIN 1 (a click invokes the helper on the field's own input — the receiver of the showPicker call is asserted) and PIN 2 (a host `onClick` arriving through DOM props is not swallowed); PIN 3 pins that one click runs BOTH, which is what a short-circuit actually breaks, while the statements' ORDER is deliberately not pinned because swapping them changes no observable outcome. The helper is NOT mocked (per the triage boundary): the real helper runs and `showPicker` is spied on the prototype, which is load-bearing because happy-dom 20.11.2 does not implement it and the helper's call is optional, so without a stand-in every assertion here would pass for an empty reason — the same route `packages/components/src/renderers/form/__tests__/form-date-picker-click.test.tsx` already takes. Only the CALL PATH is claimed; real browser picker behaviour is out of reach of this DOM and is stated as not claimed in the file header.", "tests": "All from the repo root, exit codes captured by redirecting first (cmd > log 2>&1; EXIT=$?), never through a pipe. Heavy runs went through the shared verify lock (/home/user/objectstack/scripts/pm/os-verify-lock.sh, slot dev-7570); verdicts read off its VERDICT line, not a bare $?. (1) `pnpm exec vitest run packages/fields/src/widgets/openNativePicker.test.tsx` -> 'Test Files 1 passed (1) / Tests 8 passed (8)', lock VERDICT command-exit 0. (2) Whole package on the restored tree: `pnpm exec vitest run packages/fields/` -> 'Test Files 134 passed (134) / Tests 2186 passed (2186)', lock VERDICT command-exit 0 (waited 285s in the queue first; in-turn synchronous wait, no background watcher). (3) `pnpm exec turbo run type-check --filter @object-ui/fields --concurrency=2` -> 'Tasks: 11 successful, 11 total'. NOT-MEASURED check cleared: `tsc -p packages/fields/tsconfig.test.json --listFiles` names openNativePicker.test.tsx, so 'type-check clean' is a statement about the new file rather than around it. One earlier red here was real and fixed — TS2790, because lib.dom declares showPicker as REQUIRED, so `delete` on it is a compile error; the cleanup now uses Reflect.deleteProperty. (4) `pnpm exec eslint packages/fields/src/widgets/openNativePicker.test.tsx` -> exit 0, no findings. (5) Changeset gate, verdict lines verbatim. BEFORE (exit 1): '1 source file(s) of 1 released package(s) changed, and this change adds no changeset:' naming @object-ui/fields / packages/fields/src/widgets/openNativePicker.test.tsx. AFTER (exit 0): '1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/opennativepicker-coverage-7570.md.' + 'Every one of them has an EMPTY frontmatter - declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.' `node scripts/check-changeset-no-major.mjs` -> exit 0. (6) `grep -naP` control-byte scan over the new file and every script: no hits. ABLATION — 3 legs, all run AFTER the implementation was committed (0a9ef8e1b), so HEAD genuinely holds the implementation the restore leg points at. NO BUILD/dist LEG IS INVOLVED and that is a property of the fixture, not an omission: the test imports DateField and openNativePicker by RELATIVE PATH, so Vitest transforms the mutated source on disk and no dist/ sits in the resolution path to hide a stale artifact. Each leg proved the mutation LANDED ON DISK before running — git hash-object moved AND the anchor count flipped 1 -> 0, and the harness exits non-zero rather than running the leg if either observation fails to flip (a zero-hit edit exits 0 and reads like a successful ablation). LEG A, deleted `openNativePicker(e.currentTarget);` from DateField.tsx: hash 8a472aed2 -> 5a499721a, anchor 1 -> 0; vitest exit 1, 'Tests 2 failed | 6 passed (8)', named: 'PIN 1: clicking the field invokes the helper on the field's own input' and 'PIN 3: one click runs BOTH'. LEG B, deleted `domProps.onClick?.(e);` from DateField.tsx: hash 8a472aed2 -> d37354589, anchor 1 -> 0; vitest exit 1, 'Tests 2 failed | 6 passed (8)', named: 'PIN 2: a host onClick delivered through DOM props is not swallowed' and 'PIN 3'. LEG C, deleted the disabled/readOnly guard from openNativePicker.ts: hash 26b2069ef -> 4c2ec929e, anchor 1 -> 0; vitest exit 1, 'Tests 2 failed | 6 passed (8)', named: 'stays shut for a disabled input' and 'stays shut for a readOnly input'. RESTORE, proved BY STATE and not by assuming the trap fired: `git checkout HEAD -- ABSOLUTE_PATH` (never the bare form, which restores from the INDEX and would put the mutation back), then `git diff HEAD` empty AND git hash-object equal to the HEAD blob — 8a472aed2e9e88624396dadf0dcf26f10a0feccc for DateField.tsx, 26b2069ef2781e2049272fe3b130a1d275da6f93 for openNativePicker.ts; an empty hash is read as FAILURE, not as 'nothing to compare'. A `trap restore_all EXIT INT TERM` with absolute paths (REPO_ROOT from git rev-parse --show-toplevel) covered the crash path. Final `git status --short` clean, and the 2186-test package run above was executed on that restored tree. NOT RUN, plainly: the repo-wide `pnpm lint` / `pnpm test` farms and the rest of the check:* family — CI runs them exactly once on this PR; lint was narrowed to the one changed file.", "mcp_calls": "0 - every GitHub action used repo-scoped REST with $GITHUB_TOKEN (issue read 200, comments read 200, PR create 201, PR read-back 200). MCP GitHub tools were reported rate-limited for this user in this container and were never called. Re-measured this run: /search/* is 403, repo-scoped REST is 200.", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
落地记录 —— PR #7818 已合入(本 lane 今日第 27 次落地)
merged: true@ 2026-09-06T00:30:12Z,squasha91aa7852- 2 files, +187 / −0;closing keyword 只有
#7570;卡片closed_at00:30:13Z
⭐ 两条值得留下来的读数
1. 三腿消融,而且第三腿是执行席自己加的。
我只要求两条(删
openNativePicker(...)⇒ pin 1 红;删domProps.onClick?.(e)⇒ pin 2 红)。执行席加了 leg C:删掉disabled/readOnly守卫 ⇒ 「stays shut for a disabled input」与「readOnly」两条变红。⇒ 顺手把两条没人钉过的行为也钉住了。2. ⭐⭐ 它排除了一个看起来很像的假缺陷,并说明了为什么。
openNativePicker.ts里input as HTMLInputElement & { showPicker?: () => void }这个断言,在 lib.dom 把showPicker(): void声明成必需之后看起来是冗余的。它不是 —— 运行时该属性真的可能不存在(happy-dom、老浏览器),而正是这一点让那个可选调用在类型上合法。⭐ 而执行席是从另一侧撞到同一个 lib.dom 事实才确认的:它自己写测试时用
delete清理,被TS2790拦下(因为 lib.dom 说它必需),只好改用Reflect.deleteProperty。⇒ 「这个断言是对的」不是读代码读出来的判断,是被编译器从反方向证实的。⚠️ 它也明确写下了不宣称的部分:只宣称调用路径,真实浏览器里的 picker 行为不在这个 DOM 的能力范围内 —— 写在文件头里。
Generated by Claude Code
Found while implementing #7336 (standard-widget suite coverage). Filed rather than fixed there: that card is
p3and scoped to one file's documentation-of-absence, and this is a separate gap in a different file.The reading
packages/fields/src/widgets/openNativePicker.tsis mentioned by zero test files in the repo.The control answers, so the instrument reaches helper names in test files and the zero is a real absence rather than a broken pattern.
Why it is worth recording
DateFieldis its sole router — it is called from exactly one place, theonClickhandler inpackages/fields/src/widgets/DateField.tsx:DateFielditself is well covered (packages/fields/src/datetime-widgets.test.tsxholds a full dedicated block, and__tests__/date-locale-channel.test.tsxpins the locale channel), but none of those cases fires a click, so the picker call is never exercised. The two things a test could pin that nothing currently does: that the helper is invoked at all, and that it does not swallow a host-suppliedonClickpassed through DOM props.No assignee — recording, not claiming.
Generated by Claude Code