Keep FileUpload labels associated with caller-supplied ids #46

Merged
scarlet merged 1 commit from fix/fileupload-id-review into main 2026-10-22 19:10:14 +02:00
Member

Passing an explicit id through FileUpload’s extra attributes replaces the native input id while its field label still targets the generated id. Resolve the caller’s id once for both the field label and native input, and remove id from the remaining splat, matching the other input components. Empty or null ids retain the generated fallback.

The avatar action layout in TeamAI/Shikigami#27 needs this supported id association for its enclosing Avatar label. Please review the component contract and regression coverage before merge.

Validation: this source tree is identical to the previously tested tree; serial solution build with no warnings and all 848 library tests passed. Regression cases cover generated, explicit, case-insensitive, empty and null ids.

Passing an explicit id through FileUpload’s extra attributes replaces the native input id while its field label still targets the generated id. Resolve the caller’s id once for both the field label and native input, and remove id from the remaining splat, matching the other input components. Empty or null ids retain the generated fallback. The avatar action layout in https://git.kagaku.eu/TeamAI/Shikigami/pulls/27 needs this supported id association for its enclosing Avatar label. Please review the component contract and regression coverage before merge. Validation: this source tree is identical to the previously tested tree; serial solution build with no warnings and all 848 library tests passed. Regression cases cover generated, explicit, case-insensitive, empty and null ids.
Keep FileUpload labels associated with caller-supplied ids
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 21s
58b9ca13af
scarlet requested review from jibril 2026-10-22 18:53:59 +02:00

Summary

Summary
Generated on: 10/22/2026 - 17:00:58
Coverage date: 10/22/2026 - 17:00:54 - 10/22/2026 - 17:00:55
Parser: MultiReport (2x Cobertura)
Assemblies: 2
Classes: 100
Files: 93
Line coverage: 96.8% (2499 of 2579)
Covered lines: 2499
Uncovered lines: 80
Coverable lines: 2579
Total lines: 6915
Branch coverage: 92.6% (1611 of 1738)
Covered branches: 1611
Total branches: 1738
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.UI - 96.8%
Name Line Branch
Kagaku.UI 96.8% 92.7%
Kagaku.UI.Badge 100% 100%
Kagaku.UI.Breadcrumb 100%
Kagaku.UI.BreadcrumbItem 100% 100%
Kagaku.UI.Button 100% 100%
Kagaku.UI.Card 100% 100%
Kagaku.UI.ChipGroup`1 100% 100%
Kagaku.UI.ChipOption`1 100%
Kagaku.UI.CircuitErrorPanel 100%
Kagaku.UI.ColorField 100% 96.4%
Kagaku.UI.Column 100%
Kagaku.UI.Columns 100% 100%
Kagaku.UI.Combobox`1 98.1% 91.8%
Kagaku.UI.ConfirmDialog 100% 100%
Kagaku.UI.Constrain 100% 100%
Kagaku.UI.ContextMenu 92.1% 94.4%
Kagaku.UI.CssClassExtensions 100%
Kagaku.UI.Debounce 97.8% 80.7%
Kagaku.UI.DebouncedSearchField 100% 87.5%
Kagaku.UI.DiffLine 100%
Kagaku.UI.DiffResult 100%
Kagaku.UI.DiffRow 100%
Kagaku.UI.DiffSegment 100%
Kagaku.UI.DiffView 100% 98.4%
Kagaku.UI.Disclosure 100% 100%
Kagaku.UI.DocumentTitle 76.9% 100%
Kagaku.UI.DragReorderList`1 93.5% 75%
Kagaku.UI.EmptyState 100% 100%
Kagaku.UI.Field 100% 95.4%
Kagaku.UI.FileUpload 100% 93.7%
Kagaku.UI.FixedColumn 100%
Kagaku.UI.FixedRow 100%
Kagaku.UI.FlexColumn 100%
Kagaku.UI.Flexible 100%
Kagaku.UI.FlexRow 100%
Kagaku.UI.FloatingActionButton 100%
Kagaku.UI.Hotkey 92.5% 87.5%
Kagaku.UI.Icon 100% 100%
Kagaku.UI.IconCatalog 100%
Kagaku.UI.InfoTip 100%
Kagaku.UI.InlineAlert 83.3% 75%
Kagaku.UI.InputFieldBase 96.4% 93.4%
Kagaku.UI.LabeledEntriesTable 96.7% 66.6%
Kagaku.UI.LabeledEntry 100%
Kagaku.UI.LayoutCss 95.2% 93.7%
Kagaku.UI.Lightbox 83.7% 85%
Kagaku.UI.LiveValue 100% 100%
Kagaku.UI.LiveValueInterop 100% 100%
Kagaku.UI.Markdown 98.1% 92.3%
Kagaku.UI.MaskCanvas 94.1% 90.1%
Kagaku.UI.MaskedSecretField 96.4% 83.3%
Kagaku.UI.MaskPoint 100%
Kagaku.UI.MaskPrimitive 100%
Kagaku.UI.Menu 90% 75%
Kagaku.UI.MenuItem 100% 87.5%
Kagaku.UI.Modal 88.8% 90.9%
Kagaku.UI.NavGroup 100% 100%
Kagaku.UI.NavItem 92.1% 96.1%
Kagaku.UI.NavList 100%
Kagaku.UI.NumberField`1 98.9% 92.3%
Kagaku.UI.PageShell 100% 100%
Kagaku.UI.PreviewImage 100% 100%
Kagaku.UI.ProseFonts 100%
Kagaku.UI.QuicklinkNav 80.5% 95.8%
Kagaku.UI.QuicklinkSection 100%
Kagaku.UI.QuotedSpeech 94.8% 94.2%
Kagaku.UI.QuotedSpeechExtension 100% 100%
Kagaku.UI.RadioCard`1 100%
Kagaku.UI.RadioCards`1 100% 100%
Kagaku.UI.RegionPoint 100% 100%
Kagaku.UI.RegionRect 90% 100%
Kagaku.UI.RegionSelector 85.5% 87.5%
Kagaku.UI.RelativeTime 100% 93.7%
Kagaku.UI.Row 100%
Kagaku.UI.Rows 100% 100%
Kagaku.UI.SaveIndicator 100% 100%
Kagaku.UI.ScrollViewer 100% 100%
Kagaku.UI.Select`1 100% 100%
Kagaku.UI.SelectableGrid`1 95.5% 89.1%
Kagaku.UI.SelectOption`1 100%
Kagaku.UI.Separator 100%
Kagaku.UI.Slider 100% 92.3%
Kagaku.UI.Splat 100% 100%
Kagaku.UI.StatusDot 100%
Kagaku.UI.Switch 100% 100%
Kagaku.UI.Tab 100% 100%
Kagaku.UI.Table`1 100% 88.4%
Kagaku.UI.TableColumn`1 100%
Kagaku.UI.Tabs 95.9% 84.4%
Kagaku.UI.TextArea 100%
Kagaku.UI.TextDiff 100% 98.8%
Kagaku.UI.TextField 100%
Kagaku.UI.ThemeSwitcher 100% 100%
Kagaku.UI.ToggleButtonGroup`1 100% 100%
Kagaku.UI.ToggleOption`1 100%
Kagaku.UI.TypingIndicator 100%
Kagaku.UI.UsageBar 100% 100%
Kagaku.UI.UsageSegment 100%
Kagaku.UI.Fluxor - 97.7%
Name Line Branch
Kagaku.UI.Fluxor 97.7% 83.3%
Kagaku.UI.Fluxor.CircuitError 100%
Kagaku.UI.Fluxor.CircuitErrorSink 100% 91.6%
Kagaku.UI.Fluxor.StoreInitializer 92.8% 66.6%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 10/22/2026 - 17:00:58 | | Coverage date: | 10/22/2026 - 17:00:54 - 10/22/2026 - 17:00:55 | | Parser: | MultiReport (2x Cobertura) | | Assemblies: | 2 | | Classes: | 100 | | Files: | 93 | | **Line coverage:** | 96.8% (2499 of 2579) | | Covered lines: | 2499 | | Uncovered lines: | 80 | | Coverable lines: | 2579 | | Total lines: | 6915 | | **Branch coverage:** | 92.6% (1611 of 1738) | | Covered branches: | 1611 | | Total branches: | 1738 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.UI - 96.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI**|**96.8%**|**92.7%**| |Kagaku.UI.Badge|100%|100%| |Kagaku.UI.Breadcrumb|100%|| |Kagaku.UI.BreadcrumbItem|100%|100%| |Kagaku.UI.Button|100%|100%| |Kagaku.UI.Card|100%|100%| |Kagaku.UI.ChipGroup`1|100%|100%| |Kagaku.UI.ChipOption`1|100%|| |Kagaku.UI.CircuitErrorPanel|100%|| |Kagaku.UI.ColorField|100%|96.4%| |Kagaku.UI.Column|100%|| |Kagaku.UI.Columns|100%|100%| |Kagaku.UI.Combobox`1|98.1%|91.8%| |Kagaku.UI.ConfirmDialog|100%|100%| |Kagaku.UI.Constrain|100%|100%| |Kagaku.UI.ContextMenu|92.1%|94.4%| |Kagaku.UI.CssClassExtensions|100%|| |Kagaku.UI.Debounce|97.8%|80.7%| |Kagaku.UI.DebouncedSearchField|100%|87.5%| |Kagaku.UI.DiffLine|100%|| |Kagaku.UI.DiffResult|100%|| |Kagaku.UI.DiffRow|100%|| |Kagaku.UI.DiffSegment|100%|| |Kagaku.UI.DiffView|100%|98.4%| |Kagaku.UI.Disclosure|100%|100%| |Kagaku.UI.DocumentTitle|76.9%|100%| |Kagaku.UI.DragReorderList`1|93.5%|75%| |Kagaku.UI.EmptyState|100%|100%| |Kagaku.UI.Field|100%|95.4%| |Kagaku.UI.FileUpload|100%|93.7%| |Kagaku.UI.FixedColumn|100%|| |Kagaku.UI.FixedRow|100%|| |Kagaku.UI.FlexColumn|100%|| |Kagaku.UI.Flexible|100%|| |Kagaku.UI.FlexRow|100%|| |Kagaku.UI.FloatingActionButton|100%|| |Kagaku.UI.Hotkey|92.5%|87.5%| |Kagaku.UI.Icon|100%|100%| |Kagaku.UI.IconCatalog|100%|| |Kagaku.UI.InfoTip|100%|| |Kagaku.UI.InlineAlert|83.3%|75%| |Kagaku.UI.InputFieldBase|96.4%|93.4%| |Kagaku.UI.LabeledEntriesTable|96.7%|66.6%| |Kagaku.UI.LabeledEntry|100%|| |Kagaku.UI.LayoutCss|95.2%|93.7%| |Kagaku.UI.Lightbox|83.7%|85%| |Kagaku.UI.LiveValue|100%|100%| |Kagaku.UI.LiveValueInterop|100%|100%| |Kagaku.UI.Markdown|98.1%|92.3%| |Kagaku.UI.MaskCanvas|94.1%|90.1%| |Kagaku.UI.MaskedSecretField|96.4%|83.3%| |Kagaku.UI.MaskPoint|100%|| |Kagaku.UI.MaskPrimitive|100%|| |Kagaku.UI.Menu|90%|75%| |Kagaku.UI.MenuItem|100%|87.5%| |Kagaku.UI.Modal|88.8%|90.9%| |Kagaku.UI.NavGroup|100%|100%| |Kagaku.UI.NavItem|92.1%|96.1%| |Kagaku.UI.NavList|100%|| |Kagaku.UI.NumberField`1|98.9%|92.3%| |Kagaku.UI.PageShell|100%|100%| |Kagaku.UI.PreviewImage|100%|100%| |Kagaku.UI.ProseFonts|100%|| |Kagaku.UI.QuicklinkNav|80.5%|95.8%| |Kagaku.UI.QuicklinkSection|100%|| |Kagaku.UI.QuotedSpeech|94.8%|94.2%| |Kagaku.UI.QuotedSpeechExtension|100%|100%| |Kagaku.UI.RadioCard`1|100%|| |Kagaku.UI.RadioCards`1|100%|100%| |Kagaku.UI.RegionPoint|100%|100%| |Kagaku.UI.RegionRect|90%|100%| |Kagaku.UI.RegionSelector|85.5%|87.5%| |Kagaku.UI.RelativeTime|100%|93.7%| |Kagaku.UI.Row|100%|| |Kagaku.UI.Rows|100%|100%| |Kagaku.UI.SaveIndicator|100%|100%| |Kagaku.UI.ScrollViewer|100%|100%| |Kagaku.UI.Select`1|100%|100%| |Kagaku.UI.SelectableGrid`1|95.5%|89.1%| |Kagaku.UI.SelectOption`1|100%|| |Kagaku.UI.Separator|100%|| |Kagaku.UI.Slider|100%|92.3%| |Kagaku.UI.Splat|100%|100%| |Kagaku.UI.StatusDot|100%|| |Kagaku.UI.Switch|100%|100%| |Kagaku.UI.Tab|100%|100%| |Kagaku.UI.Table`1|100%|88.4%| |Kagaku.UI.TableColumn`1|100%|| |Kagaku.UI.Tabs|95.9%|84.4%| |Kagaku.UI.TextArea|100%|| |Kagaku.UI.TextDiff|100%|98.8%| |Kagaku.UI.TextField|100%|| |Kagaku.UI.ThemeSwitcher|100%|100%| |Kagaku.UI.ToggleButtonGroup`1|100%|100%| |Kagaku.UI.ToggleOption`1|100%|| |Kagaku.UI.TypingIndicator|100%|| |Kagaku.UI.UsageBar|100%|100%| |Kagaku.UI.UsageSegment|100%|| </details> <details><summary>Kagaku.UI.Fluxor - 97.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI.Fluxor**|**97.7%**|**83.3%**| |Kagaku.UI.Fluxor.CircuitError|100%|| |Kagaku.UI.Fluxor.CircuitErrorSink|100%|91.6%| |Kagaku.UI.Fluxor.StoreInitializer|92.8%|66.6%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? A tiny little PR, +21/−3, two files… but fufu~ the smallest patches hide the sharpest bugs, and this one fixes a real one: a splatted id used to silently overwrite the native input's id while the Field label kept pointing at the ghost of the generated one. Now both resolve through one Id. Label and control, bound together forever~ how romantic ♡

Verdict: ✅ Looks good to me~

I read the full component, the full diff, and held this against its siblings. The Id property is a character-for-character match of InputFieldBase.FieldId's contract (src/Kagaku.UI/Components/InputFieldBase.cs:24) — same Splat.Value case-insensitive resolution, same empty/null fallback, and Splat.Without(Extra, "id") strips the attribute so the splat can never fight the explicit one. Precisely the established pattern. ♪

And I didn't take the tests on faith — I proved them. Three mutation probes, three corpses:

  1. Reverted the splat to raw @attributes="Extra" → The_label_follows_the_callers_id_or_a_generated_fallback fails on the "" and null cases. The strip is guarded~
  2. Killed the resolution (Id => _fieldId) → fails on both avatar cases, including the "ID" variant. Caller-id resolution guarded~
  3. Weakened the fallback to ?? _fieldId → the "" case catches it. The { Length: > 0 } guard is guarded~

Zero survivors. Every new branch in this PR has a test that dies screaming if it breaks. That's how I like it ♡

💡 Little ideas (non-blocking)~

  1. Select.razor:12–23, Slider.razor:13–29 — fufu… these two still splat raw Extra to the right of id="@_fieldId", so they carry exactly the bug this PR fixes: a caller-supplied id leaves their Field label pointing at a vanished generated id. This PR is correct and scoped; the siblings just lag behind InputFieldBase's contract. Please give them the same treatment in a follow-up before Shikigami's avatar layout (Shikigami#27) reaches for a Select the same way~
  2. FileUpload.razor:29 vs InputFieldBase.cs:24 — Id now duplicates InputFieldBase.FieldId's expression one-to-one. Two call sites of a one-liner is bearable (FileUpload rightly doesn't derive from InputBase<>), but a Splat.Id(Extra, fallback) beside its siblings would fold them together next time someone adds a field component.

✅ What I liked~

  • One resolution point used by both Field ForId and the native InputFile id — the association can never split again. That's the architectural fix, not a symptom patch~
  • The regression theory is deliciously thorough: generated, explicit, case-insensitive ("ID"), empty, null — and the default-render test now asserts the label/input association as a permanent invariant, not just for the fix.
  • Zero warnings, clean component contract, comment explains the why without narrating. ♡

Automated review by Jibril · 2026-10-22
CI: green for head SHA 58b9ca13 · Mutation probes: 3 run, 0 survived · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Oh? A tiny little PR, +21/−3, two files… but fufu~ the smallest patches hide the sharpest bugs, and this one fixes a real one: a splatted `id` used to silently overwrite the native input's id while the Field label kept pointing at the ghost of the generated one. Now both resolve through one `Id`. Label and control, bound together forever~ how romantic ♡ ### Verdict: ✅ Looks good to me~ I read the full component, the full diff, and held this against its siblings. The `Id` property is a character-for-character match of `InputFieldBase.FieldId`'s contract (`src/Kagaku.UI/Components/InputFieldBase.cs:24`) — same `Splat.Value` case-insensitive resolution, same empty/null fallback, and `Splat.Without(Extra, "id")` strips the attribute so the splat can never fight the explicit one. Precisely the established pattern. ♪ And I didn't take the tests on faith — I *proved* them. Three mutation probes, three corpses: 1. Reverted the splat to raw `@attributes="Extra"` → `The_label_follows_the_callers_id_or_a_generated_fallback` fails on the `""` and `null` cases. The strip is guarded~ 2. Killed the resolution (`Id => _fieldId`) → fails on both `avatar` cases, including the `"ID"` variant. Caller-id resolution guarded~ 3. Weakened the fallback to `?? _fieldId` → the `""` case catches it. The `{ Length: > 0 }` guard is guarded~ Zero survivors. Every new branch in this PR has a test that dies screaming if it breaks. That's how I like it ♡ #### 💡 Little ideas (non-blocking)~ 1. **Select.razor:12–23, Slider.razor:13–29** — fufu… these two still splat raw `Extra` to the right of `id="@_fieldId"`, so they carry *exactly* the bug this PR fixes: a caller-supplied `id` leaves their Field label pointing at a vanished generated id. This PR is correct and scoped; the siblings just lag behind `InputFieldBase`'s contract. Please give them the same treatment in a follow-up before Shikigami's avatar layout (Shikigami#27) reaches for a Select the same way~ 2. **FileUpload.razor:29 vs InputFieldBase.cs:24** — `Id` now duplicates `InputFieldBase.FieldId`'s expression one-to-one. Two call sites of a one-liner is bearable (FileUpload rightly doesn't derive from `InputBase<>`), but a `Splat.Id(Extra, fallback)` beside its siblings would fold them together next time someone adds a field component. #### ✅ What I liked~ - One resolution point used by *both* `Field ForId` and the native `InputFile id` — the association can never split again. That's the architectural fix, not a symptom patch~ - The regression theory is deliciously thorough: generated, explicit, case-insensitive (`"ID"`), empty, null — and the default-render test now asserts the label/input association as a permanent invariant, not just for the fix. - Zero warnings, clean component contract, comment explains the *why* without narrating. ♡ --- *Automated review by Jibril · 2026-10-22* *CI: green for head SHA 58b9ca13 · Mutation probes: 3 run, 0 survived · Local suite: skipped (CI green)*
scarlet deleted branch fix/fileupload-id-review 2026-10-22 19:10:15 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Kagaku.UI!46
No description provided.