a1e7909f - Separate the action POST from the detail refresh in the compliance stop/resume flow - #1263
Conversation
…op/resume flow A refresh failure after a successful stop or resume is no longer reported as a failed action: the dialog closes and onStatusChanged fires right after the POST, and a failing getTransactionByUid only invalidates the cached detail and surfaces its own error instead of keeping a stale entry. ConfirmDialog no longer forwards onClose while a request is running, so Escape and backdrop clicks cannot dismiss the dialog mid-request, and each dialog's loading state is bound to the transaction it was opened for.
The layout context mock referenced `document` inside the jest.mock factory, which Jest rejects, so both new suites failed to run at all. The root ref now comes from a mock-prefixed holder that beforeEach fills in. The refresh is driven by a controlled promise, so the tests now prove that the dialog closes and onStatusChanged fires while the refresh is still pending, and that a refresh error is rendered in the transaction detail row instead of the action error field. A new case with two transactions covers the per-transaction loading state, and the interactions are wrapped in act to keep the console clean.
…saction Two overlap races remained once an action for a second transaction could be started while the first one was still refreshing. The loading state is a single id, so the finally block of the first action reset it while the POST of the second was still in flight, releasing that dialog early. Each action now clears the state only if it is still its own. The detail error was a single string shared with handleUidClick, so a refresh that settled late wiped or replaced the error belonging to a transaction the user had opened in the meantime, leaving its detail row blank. The error now carries the uid it belongs to and is only cleared, set and rendered for that transaction.
Binding the single error slot to a uid only covered the success path: when a refresh failed, it still replaced whatever error was on screen, so the detail row of a transaction the user had opened in the meantime went blank — the very case the previous commit set out to fix. Detail errors are now kept in a map keyed by uid, mirroring the detail cache next to it. Each path only touches the entry of the transaction it belongs to, and the row renders its own error or nothing.
|
Four review passes were needed to reach a clean state, each one covering both conformance and logic:
Beyond the automated checks, both suites were run against eight deliberate mutations of the production code — loading state unscoped, The last pass surfaced three further races in neighbouring scalar states (action tracking overwritten by a second action, |
Closes #1256.
Two robustness gaps in the compliance transactions tab, both pre-existing in the stop flow and mirrored by the resume flow added in #1254.
1. A failing detail refresh no longer reports the action as failed
confirmStop/confirmResumeran the action POST and the follow-upgetTransactionByUidrefresh in onetry. When only the refresh failed after a successful POST, the UI showed Stop failed / Resume failed, kept the dialog open, skippedonStatusChangedand left the stale cache entry in place — so the action was offered again although it had already succeeded server-side.The POST now has its own
try/catchand returns early on failure. On success the dialog closes andonStatusChangedfires immediately; the refresh runs afterwards through a newrefreshTxDetailhelper that, on failure, drops the cached detail and reports its own error instead of masking it as an action failure.2. Escape and backdrop clicks are blocked while a request is running
ConfirmDialogpassedonCancelstraight through as theModalonClose, andModalinvokes it on Escape and backdrop click without looking atisLoading— only the Cancel button was disabled. The dialog could therefore be dismissed mid-request and another one opened, leaking the sharedstoppingTxId/resumingTxIdloading state into the wrong dialog instance.ConfirmDialognow forwardsonCloseonly when it is not loading, and each dialog'sisLoadingis bound to the transaction it was opened for.3. Two shared states that this made reachable
Because an action for a second transaction can now legitimately be started while the first one is still refreshing, two states shared across transactions needed the same scoping:
stoppingTxId/resumingTxIdhold a single id, so thefinallyof the first action reset it while the POST of the second was still in flight and released that dialog early. Each action now clears the state only if it is still its own.txDetailErrorwas a single slot shared withhandleUidClick, so a refresh settling late wiped or replaced the error of a transaction the user had opened in the meantime, leaving its detail row blank. Detail errors are now kept in aMapkeyed by uid — mirroring the detail cache next to it — and every path only touches the entry of the transaction it belongs to.Tests
src/__tests__/compliance-transaction-action-refresh.test.tsx:onStatusChangedfired — and the refresh error is asserted to render inside the transaction detail row rather than in the action error field;src/__tests__/confirm-dialog-loading.test.tsx: Escape and backdrop click are ignored while loading and still close the dialog when idle.Both suites were checked against deliberate mutations of the production code — loading state unscoped,
onClosepassed through unguarded, stale cache kept, dialog closed only after the refresh, refresh error routed into the action error field,finallyclearing a foreign loading state, and the detail error cleared or overwritten across transactions — and each one is caught by the tests that cover it.