diff --git a/app/create/__tests__/page.test.tsx b/app/create/__tests__/page.test.tsx index 73a7759..c4e54e6 100644 --- a/app/create/__tests__/page.test.tsx +++ b/app/create/__tests__/page.test.tsx @@ -405,3 +405,81 @@ describe('CreatePage — contract recipient warning (issue #392)', () => { cleanup(root, container); }); }); + +describe('CreatePage — recipient check debounce (#466)', () => { + beforeEach(() => { + mockCheckRecipientExists.mockReset().mockResolvedValue(true); + }); + + it('does not call checkRecipientExists immediately on keystroke', async () => { + const { container, root } = renderCreatePage(); + const recipientInput = container.querySelector('input[placeholder="G…"]') as HTMLInputElement; + + await act(async () => { + setFieldValue(recipientInput, TEST_RECIPIENT); + // Do NOT wait for the debounce — check immediately + }); + + expect(mockCheckRecipientExists).not.toHaveBeenCalled(); + + // Now wait for the debounce + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 650)); + }); + + expect(mockCheckRecipientExists).toHaveBeenCalledTimes(1); + cleanup(root, container); + }); + + it('fires only one check after rapid keystrokes within the debounce window', async () => { + const { container, root } = renderCreatePage(); + const recipientInput = container.querySelector('input[placeholder="G…"]') as HTMLInputElement; + + // Type rapidly — each keystroke within the 600ms debounce window + await act(async () => { + for (let i = 0; i < 5; i++) { + setFieldValue(recipientInput, TEST_RECIPIENT.slice(0, 50 + i)); + await new Promise(resolve => setTimeout(resolve, 100)); + } + setFieldValue(recipientInput, TEST_RECIPIENT); + await new Promise(resolve => setTimeout(resolve, 50)); + }); + + // Should not have fired yet (last keystroke was only 50ms ago) + expect(mockCheckRecipientExists).not.toHaveBeenCalled(); + + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 650)); + }); + + expect(mockCheckRecipientExists).toHaveBeenCalledTimes(1); + cleanup(root, container); + }); + + it('aborts the in-flight check when the address changes mid-debounce', async () => { + const { container, root } = renderCreatePage(); + const recipientInput = container.querySelector('input[placeholder="G…"]') as HTMLInputElement; + + // Type first address and wait for debounce to fire + await act(async () => { + setFieldValue(recipientInput, TEST_RECIPIENT); + await new Promise(resolve => setTimeout(resolve, 650)); + }); + + expect(mockCheckRecipientExists).toHaveBeenCalledTimes(1); + + // Change address before the check completes + const SECOND_RECIPIENT = 'GCBBG5LDGECWWCJN7NGP6JIVY6M2PDMZXHFIWDBMR5WKZFGF5NPOILFH'; + mockCheckRecipientExists.mockClear(); + + await act(async () => { + setFieldValue(recipientInput, SECOND_RECIPIENT); + // The old check should be aborted — only the new one should fire + await new Promise(resolve => setTimeout(resolve, 650)); + }); + + expect(mockCheckRecipientExists).toHaveBeenCalledTimes(1); + expect(mockCheckRecipientExists).toHaveBeenCalledWith(SECOND_RECIPIENT, expect.any(Object)); + cleanup(root, container); + }); +});