← All Reports

Currency Entry Widget — ENG-5409

Two-box currency control · searchable ISO-4217 picker · canonical amount normalization · nested response · state variants · Storybook & visual-editor authoring

📅 2026-07-03 🌐 form.dev / our.forms.dev.sense.engineering (DEV) + Storybook :6006 / editor-demo :4200 (local) 🤖 Claude Opus 4.8 + playwright-cli + GraphQL 👤 Paul Wagner
PARTIAL
Overall result
77 / 80
TC steps passed
3
Failures
0
Blocking bugs
6
Test cases

📝 Verdict — PARTIAL

The <currency-entry> widget renders as the two-box currency control on the live DEV renderer, the searchable ISO-4217 picker works, the amount normalizes to a canonical period-decimal string, and submissions return the nested map { "Type":"CURRENCYENTRY", "Value":{ "StandardCode","Value" }, "Label","State" } with no ResolvedValue. All four states render and submit their State. Storybook coverage is complete and matches the mobile reference, and the widget is authorable end-to-end in the visual editor.

Three failures: (1) autofocus='true' does not focus the amount field on load (Low, renderer); (2) the picker does not place keyboard focus in its search field on open, so arrow-key navigation is inert until the field is clicked (Medium, renderer); (3) the visual editor's Show SFML emits style="0" where the test expected style="LARGE" (Low, editor — intended numeric serialization that round-trips through the renderer, likely a stale expectation).

Method note: TC1-TC4 driven on DEV via the OrgApp GraphQL (sessionStart + anonymous sessionShareIssue) same-origin from the authenticated browser, opened in the ViewerApp; shadow-DOM values read via getComputedStyle / native input value; submissions via session.stage.submittedValues. TC5/TC6 ran locally (Storybook :6006, editor-demo :4200). Node v22.11 sat one patch below the Angular CLI v22.12 floor, so the two local dev servers needed a version-gate shim + --experimental-require-module + a missing oxc-parser binding to start — none of which affect the widget under test.

TC1 — Renders in the Forms Web Renderer  11 PASS / 1 FAIL

#CheckResultEvidence
1–2Two-box control renders (not unsupported fallback)PASSbutton "Currency" (USD) + textbox "Amount amount"
3–4Label above, caption belowPASS"Amount" above, "Total due" below the control
5Empty caption → none shownPASScaption='' → no caption node
6–7Seeded currency USD / amount 19.99PASScurrency "USD", amount value "19.99"
8No amount → "0.00" placeholderPASSinitvalue='' → placeholder "0.00", empty value
9No currency → em-dashPASScurrency box "—"
10autofocus focuses the amount fieldFAILafter load and reload, deepest activeElement=BODY (hasFocus true); component has no .focus() wiring
11Long label/caption — row stays alignedPASSboth boxes top=499 (5px gap); label wrapped to 2 lines above
12SMALL identical to LARGEPASSSMALL 70x43 / 429x43 = LARGE exactly

TC2 — Searchable Currency Picker  13 PASS / 1 FAIL

#CheckResultEvidence
1Empty currency boxPASScurrency box "—"
2Click → searchable list opensPASStextbox "Search currency" + listbox below the [expanded] button
3Option shows "(CODE) Name"PASSe.g. "(USD) US Dollar"
4"gbp" case-insensitive filterPASSfiltered to one option "(GBP) British Pound"
5"zzzz" → No matchesPASSlistitem "No matches"
6–7Click "(EUR) Euro" → box EUR, list closesPASScurrency box "EUR"; button no longer [expanded]
8Reopen + Down arrow moves highlightFAILfocus stays on the trigger button; highlight did not move. Only moves once the search field is focused (EUR→FJD)
9–10Enter selects / Escape closes (unchanged)PASS*work once the search field is focused (same focus gap as #8)
11Click outside closesPASSlistbox present → absent after outside click
12Preselected EUR highlighted as selectedPASSEUR is aria-activedescendant and the only aria-selected option
13Distinct accessible namesPASS"Currency" vs "Amount amount"
14Non-standard code XBT shown unchangedPASScurrency box "XBT"

*Enter and Escape are handled on the search input and work correctly once it has focus; they are inert immediately after opening because the picker leaves focus on the trigger button (see the three failures).

TC3 — Amount Normalization & Response Round-Trip  15 / 15 PASS

#Input / CheckResultEvidence
1Empty amount + SubmitPASSplaceholder "0.00" + Submit button
219,99PASS19.99
342.50PASSkept 42.50 (not 42.5)
41.234,56PASS1234.56
51e3PASS13, "e" rejected
6+1,000PASS1.000; "+" and "," absent (lone comma read as the decimal mark per the documented rightmost-separator rule)
7-50.00PASS-50.00, leading "-" accepted
830-digit inputPASScapped at 22 chars
9–12Submit EUR / 19,99PASSValue:{"StandardCode":"EUR","Value":"19.99"}, Type "CURRENCYENTRY", no ResolvedValue
13Submit 42.50PASSValue 42.50 (no precision loss)
14Seeded USD/19.99, submit untouchedPASS{"USD","19.99"} (not nulls)
15Bare form, submit untouchedPASS{"StandardCode":"","Value":""} (empty strings, not nulls)

TC4 — State Variants  15 / 15 PASS

StateRender / behaviourSubmitted StateResult
DEFAULTEnabled, editable"DEFAULT" (25.00)PASS
READ_ONLYSeeded "1234.56"; both boxes disabled + greyed (bg rgb(245,245,245)); rejects typing; picker won't open"READ_ONLY"PASS
WARNINGAmber border both boxes rgb(255,193,7) + amber "?" icon (icon_144.svg); editable"WARNING"PASS
ERRORRed border both boxes rgb(239,68,68) + red "!" icon (icon_150.svg); amount input aria-invalid"ERROR"PASS

TC5 — Storybook Coverage  14 / 14 PASS

Sidebar lists Widgets / CurrencyEntry / Docs; the Docs page renders all 9 stories. Each story inspected in its standalone iframe.

StoryExpectedResult
Amounttwo-box, "0.00" placeholderPASS
Prefilledcurrency EUR, amount 42.50PASS
NoCurrencyempty currency boxPASS
Smallidentical to LARGE (70x43 / 861x43)PASS
LongLabelrow stays alignedPASS*
Required"Required" marker shownPASS
Warningamber borders + amber "?" icon (144)PASS
Errorred borders + red "!" icon (150), aria-invalidPASS
ReadOnlygreyed/disabled boxes, fixed amount 1234.56PASS
Corpus / RealWorldtwo-box control, no fallbackPASS
Mobile referencetwo-box proportions + amber/red treatment matchPASS

*LongLabel: the two-box row stays aligned; in Storybook's ~968px container the long label fits on one line rather than wrapping (wrapping confirmed live in TC1).

TC6 — Authoring in the Visual Form Editor  9 PASS / 1 FAIL

#CheckResultEvidence
1Editor loads with palette + canvasPASS"SFML Form Editor Demo", Widgets palette, canvas
2Input palette lists "Currency Entry"PASSdescribed "Currency code + amount input"
3Drag → seeded Amount / USD / 0.00PASSeditorCells 0→1, dropZones 1→0
4–5Property panel: Common + Currency Entry sections/fieldsPASSInitial Currency (ISO code), Initial Amount, Placeholder
6Set Initial Currency EUR → canvas EURPASScanvas currency box "EUR"
7Set Initial Amount 42.50 → canvas 42.50PASScanvas amount box "42.50"
8Show SFML exactly matches expectedFAILemits style="0" vs expected style="LARGE" (also attribute order + html/body wrapper)
9Markup includes device-required attrsPASSflow, style, label, caption, inittext, autofocus present
10Clear currency/amount → attrs omittedPASSinitcurrency / initvalue omitted from cleared SFML

The three failures

🟡 Low — TC1 step 10: autofocus='true' not honored (web renderer)

With the widget's autofocus='true', the amount field does not receive focus on load. After load and reload the deepest document.activeElement is BODY with document.hasFocus()===true; the component has no .focus() call though autofocus is parsed and modeled. Same gap logged in the 2026-06-19 report for ENG-5373. Convenience feature, no data impact.

🟠 Medium — TC2 step 8: picker does not focus its search field on open (web renderer)

Opening the picker leaves keyboard focus on the currency trigger button, whose keydown handler only opens the popup and is a no-op while open. The Down/Up/Enter/Escape handlers live on the search input, which is never auto-focused — so keyboard navigation is inert until the user clicks into the search field. Once focused, the handlers are correct (Down moved EUR→FJD, Enter selected, Escape closed). Accessibility-relevant.

🟡 Low — TC6 step 8: Show SFML emits style="0" not style="LARGE" (visual editor)

The editor serializes currency-entry style numerically ('1' for SMALL, else '0'), consistent with how flow is emitted ('0'). The test (and the 2026-07-01 runbook) expected the enum name style="LARGE". The editor's output round-trips cleanly through the live renderer (parsed with no errors, rendered as the two-box control with EUR/42.50), so this is a serialization-format difference rather than a functional break — likely a stale expectation. Because SFML is a frozen-once-prod contract, the canonical form (numeric "0" vs string "LARGE") is worth a conscious decision.