fix(TWO-25554): retire a whole capture, scope the write record, and give each panel its own notice - #414
Conversation
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…dentity Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…re diff Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tives Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ess form Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e harness's divergences Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…related one Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… failure diff Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ount moves Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The mirror carries the company fields alone, so no panel's UI state travels into the identity the tile and order-intent read. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ding Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…xport Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…carries Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The jQuery stub defines length, find and closest, so the missing-method cases soleAddressForm's guards claimed to cover cannot occur. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ding Each fails under a mutation of the guard it names: the two identity guards in company-search, the quote's no-billing-address branch, _chromeNode's sibling-only scoping, and the billing panel's own-root country fallback. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_ownFormRoot's ceiling is the panel's own address form, and the layout processor injects exactly one company_id field per form, so no ancestor inside that ceiling can hold two. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the diff (JS panel/chrome/address-write fixes + accompanying tests, plus the layout-processor PHP test). Traced through the key changed paths:
company-identity.jsclear()now retiressoleTraderAdopted/captureMode/addressNoticealongside name/id, with the guard updated to match;snapshot()correctly narrows to just the company fields.company-search.jsaddressWriteRecordre-keyed from the form element to the calling panel's identity (WeakMap) — survives subtree replacement, and each call site (company-capture.js,address-autocomplete.js, gateway sole-trader writeback) passes the right identity through.company-capture-component.jsrefreshMount(): chrome removal now happens before_boundSelectoris reassigned, andfieldNode()/_chromeHost()read off_boundSelector, so_removeChrome()correctly targets the old host being left, not the new one.onCountryChanged()readsisSoleTrader()beforeclear()resets the mode, fixing the round-trip-back-into-search bug described in the PR.announceAddressUnavailable(fetch failure) and the newannounceAddressUndeliverable(no destination) are kept distinct and each call site was updated consistently — no orphaned references.- The billing/shipping write-target split (
billingWriteRoot/billingWriteTarget) mirrors the existing shipping helpers and keys off livemountSelector()rather than static selector matching, matching the "hidden fieldset" scenario described.
No security, correctness, or breaking-API issues found. Test coverage for each fix (retirement, notice scoping, write-record keying, chrome cleanup on move) looks thorough and each is tied to a specific regression scenario.
Ready for human approval ✅
Costs nothing and guards against other faults that could cause inadvertent double rendering, independent of _ownFormRoot's ceiling. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Only one commit landed since the prior review (2de9bb1): it restores the "exactly one restored-number match" guard in _restoredCompanyNumber() (company-capture-component.js), reverting an earlier removal. It's a narrow, defensive change — climbs to the next ancestor unless exactly one match is found under the current one — and is now pinned by the two new fixtures added to LayoutProcessorPluginTest.php::untouchedBillingNodeProvider(). No new risk introduced.
The rest of the diff (capture retirement in clear(), chrome-cleanup ordering in refreshMount(), the address-write record re-keyed from DOM element to panel identity via WeakMap, and per-panel address notices replacing the shared tile notice) matches the previous review's findings — no security, correctness, or breaking-API issues.
Ready for human approval ✅
Closes the review findings left open after #410 and #411 merged. Deliberately carries no change to how "is billing a distinct address" is answered —
billingIsDistinct()is stillstaging's ownisVisible($(BILLING_FIELD_SELECTOR)). That question is a separate decision and is on its own PR.view/frontend/web/js/model/company-source-resolver.jsis code-identical tostaging; the only difference in the whole file is comments.What this closes
clear()retired only half a capture. It reset the company name, number and source but leftsoleTraderAdopted,captureModeandaddressNoticestanding, so re-checking "same as shipping" left the billing panel to remount in sole-trader mode over an empty identity. It now retires all of them (soleTraderAvailablestays — it is a property of the country, not of the capture). The test that claimed to pin this asserted only the name and number, so it passed while the adoption survived; it now asserts the mode and the adoption too.A moved mount left a stale combobox behind. Chrome removal already fired on a mount move, but nothing stripped
role,aria-haspopup,aria-controlsandaria-expandedoff the field the panel had left, so screen readers were offered a combobox that no longer had a popover. One helper now does it, shared by the move path,releaseField()andunmount().The address write record was keyed on the form element. Fire Checkout and Amasty replace the whole
payments-listsubtree — which is where the billing form lives — so the key died with the fieldset and a later retraction found nothing on record. It is now keyed on the calling panel's own identity, which survives the DOM being replaced and which no other panel holds a reference to.Address notices crossed panels.
snapshot()copied an identity's whole state, soaddressNoticetravelled through the resolver, and the payment tile — bound to the resolved identity — was the only renderer of it anywhere. A shipping-side failure while billing was authoritative displayed nothing at all, and a billing-side failure surfaced in the tile with copy pointing at the wrong form. Each panel now renders its own notice at its own field, through the same chrome mechanism as the company number and the sole-trader link, andsnapshot()carries only the company name, number and source.Billing had no write destination of its own. Its address and telephone write-backs went to a raw form-root selector while shipping used a write target that announces when there is nowhere to write. Billing now has the matching pair, so a sole-trader write-back with the fieldset gone tells the buyer instead of failing silently. That refusal gets its own string rather than reusing one that says "please enter it below" on the path where there is no form to enter it into.
Three tables' row descriptions never reached an assertion. The callback took
(actor, fixture), so the description formatted the fixture object and vanished. Converted to the tagging helper, as the rest of the suite already does.Also removed: four guards shaped around a test double's deficiency, with the comments that claimed a double required them; and an undiscriminated multi-match check, after establishing by construction that the layout processor injects exactly one
company_idper form root.Tests
53 suites / 929 tests. Every one of the 22 commits green on its own.
Five previously-unpinned invariants now have discriminating tests, each proven by applying the mutation that should break it. Three vacuity classes were found and fixed while getting here: negative assertions running before the
setTimeout(0)identity publish they denied; a write reusing a value the identity already held, so no notify fired and a stale mirror satisfied the assertion; and a harness stub returning a constant empty string, which had silently made every fallback-country assertion vacuous.No production PHP changes — the only PHP touched is the layout-processor test, which gains rows for the widened billing predicate's own false-positive shapes.
Still owed
Real nb/nl/sv translations for one new string, currently echoing its English source at
i18n/nb_NO.csv:314,nl_NL.csv:310,sv_SE.csv:311.No post-change browser verification — the staging shop runs
staging.