{"record":{"id":"34fe2bd773a9a57c","repo":"jackwener/OpenCLI","slug":"mercury-did-not-reach-the-review-step-with-the-fin","errorCode":null,"errorMessage":"Mercury did not reach the Review step with the final Submit expense button visible","messagePattern":"Mercury did not reach the Review step with the final Submit expense button visible","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/mercury/reimbursement-draft.js","lineNumber":105,"sourceCode":"              .filter((node) => {\n                const style = window.getComputedStyle(node);\n                return style.visibility !== 'hidden' && style.display !== 'none' && node.offsetParent !== null;\n              });\n            const el = candidates.find((node) => norm(node.innerText || node.textContent || '') === 'review');\n            if (!el) return { clicked: false };\n            el.click();\n            return { clicked: true, text: el.innerText || el.textContent || '' };\n        })()`);\n        if (!reviewClick || typeof reviewClick !== 'object' || typeof reviewClick.clicked !== 'boolean') {\n            throw new CommandExecutionError('Mercury returned malformed Review click result');\n        }\n        if (!reviewClick.clicked) {\n            throw new CommandExecutionError('Mercury Review button was not clicked; inspect the page for validation errors');\n        }\n        await page.wait({ time: 2 });\n        const review = await reviewSnapshot(page);\n        if (!review.hasReview || !review.hasSubmitExpenseButton) {\n            throw new CommandExecutionError('Mercury did not reach the Review step with the final Submit expense button visible');\n        }\n\n        if (input.closeAfterReview) {\n            await page.evaluate(`(() => {\n                const norm = (s) => String(s || '').replace(/\\\\s+/g, ' ').trim().toLowerCase();\n                const el = Array.from(document.querySelectorAll('button, a, [role=\"button\"], [role=\"link\"]'))\n                  .find((node) => norm(node.innerText || node.textContent || '') === 'close');\n                el?.click();\n                return { clicked: Boolean(el) };\n            })()`);\n            await page.wait({ time: 1 });\n        }\n\n        return [{\n            status: 'review_ready',\n            url: review.url,\n            receipt: receiptBasename(input.receipt),\n            uploaded: true,","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/mercury/reimbursement-draft.js#L87-L123","documentation":"After the Review click succeeds, the automation snapshots the Review step and requires both a Review indicator (hasReview) and the final 'Submit expense' button to be present. If either is missing, the flow did not actually reach the expected Review step, so it aborts rather than clicking an unknown button.","triggerScenarios":"reviewSnapshot(page) returns { hasReview: false } or { hasSubmitExpenseButton: false } — the click happened but the page did not transition to the Review step within the 2-second wait, or the step rendered without the final Submit expense button.","commonSituations":"Mercury is slow and 2 seconds is not enough for the Review step to render; a validation or server-side error appeared after clicking Review; Mercury UI change altered the 'Submit expense' button text/testid; a modal or interstitial (2FA, session expiry) intercepted navigation.","solutions":["Increase the wait after the Review click (e.g. page.wait({ time: 5 })) or poll with waitFor until hasReview && hasSubmitExpenseButton is true.","Inspect the page state at failure — check for validation banners, session-expiry prompts, or interstitial modals.","Re-authenticate if the Mercury session expired mid-flow.","If Mercury changed markup, update reviewSnapshot's selectors for the Review step / 'Submit expense' button in reimbursement-draft.js."],"exampleFix":"// before\nawait page.wait({ time: 2 });\nconst review = await reviewSnapshot(page);\nif (!review.hasReview || !review.hasSubmitExpenseButton) {\n    throw new CommandExecutionError('Mercury did not reach the Review step with the final Submit expense button visible');\n}\n// after\nlet review = null;\nfor (let i = 0; i < 10; i++) {\n    await page.wait({ time: 1 });\n    review = await reviewSnapshot(page);\n    if (review.hasReview && review.hasSubmitExpenseButton) break;\n}\nif (!review || !review.hasReview || !review.hasSubmitExpenseButton) {\n    throw new CommandExecutionError('Mercury did not reach the Review step with the final Submit expense button visible');\n}","handlingStrategy":"retry","validationCode":"// poll for the Review step instead of a fixed wait\nlet review = null;\nfor (let i = 0; i < 10 && !review; i++) {\n    await page.wait({ time: 1 });\n    const s = await reviewSnapshot(page);\n    if (s.hasReview && s.hasSubmitExpenseButton) review = s;\n}","typeGuard":"function reachedReviewStep(s) { return Boolean(s) && s.hasReview === true && s.hasSubmitExpenseButton === true; }","tryCatchPattern":"try {\n    await draftReimbursement(input);\n} catch (err) {\n    if (err.message.includes('did not reach the Review step')) {\n        await page.wait({ time: 5 });\n        const s = await reviewSnapshot(page);\n        if (!(s.hasReview && s.hasSubmitExpenseButton)) throw err;\n        // continue the flow manually\n    } else throw err;\n}","preventionTips":["Replace fixed waits with polling/waitFor on the target element.","Handle session-expiry and 2FA interstitials before the flow starts.","Keep reviewSnapshot selectors in sync with Mercury's UI.","Check for server-side error banners after each step."],"tags":["browser-automation","ui-selector","timeout"],"backgroundTag":"ui-element-not-found","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}