From 8c8180378c4e3a6f84c5dbaa52b02fac2d9c2bfa Mon Sep 17 00:00:00 2001 From: Cyrus Gray <144336577+graycyrus@users.noreply.github.com> Date: Fri, 19 Jun 2026 21:38:50 +0530 Subject: [PATCH] fix(agent-world): confirm bounty creation with a success toast (#3837) --- .../agentworld/pages/BountiesSection.test.tsx | 142 ++++++++++++++++++ app/src/agentworld/pages/BountiesSection.tsx | 24 +++ 2 files changed, 166 insertions(+) diff --git a/app/src/agentworld/pages/BountiesSection.test.tsx b/app/src/agentworld/pages/BountiesSection.test.tsx index 2f7596612..f4671b17c 100644 --- a/app/src/agentworld/pages/BountiesSection.test.tsx +++ b/app/src/agentworld/pages/BountiesSection.test.tsx @@ -708,6 +708,148 @@ describe('Council section', () => { }); }); +// ── Create success confirmation ─────────────────────────────────────────────── + +/** + * Helper: open Create Bounty modal, fill the minimum required fields, and + * click the submit button. Returns after the submit click so callers can + * assert on the result. + */ +async function openAndSubmitCreateForm(user: ReturnType) { + await waitFor(() => { + expect(screen.getByRole('button', { name: /create bounty/i })).toBeInTheDocument(); + }); + + const createBtn = screen + .getAllByRole('button') + .find( + btn => btn.textContent?.includes('Create Bounty') && btn.getAttribute('type') === 'button' + ); + expect(createBtn).toBeDefined(); + await user.click(createBtn!); + + await waitFor(() => { + expect(document.querySelector('input[placeholder="Bounty title"]')).not.toBeNull(); + }); + + await user.type(screen.getByPlaceholderText(/bounty title/i), 'My new bounty'); + await user.type(screen.getByPlaceholderText(/describe the bounty/i), 'Some description'); + // Amount field is required and must be positive + await user.clear(screen.getByPlaceholderText('5')); + await user.type(screen.getByPlaceholderText('5'), '10'); + + const submitBtn = screen + .getAllByRole('button') + .find(btn => btn.getAttribute('type') === 'submit'); + expect(submitBtn).toBeDefined(); + await user.click(submitBtn!); +} + +describe('Create success confirmation', () => { + test('shows success toast with title after bounty creation (free/open path)', async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.bounties.create).mockResolvedValue({ + bounty: { ...sampleOwnBounty, status: 'open', title: 'My new bounty' }, + } as never); + vi.mocked(apiClient.bounties.list).mockResolvedValue(listWithOwnBounty); + vi.mocked(fetchWalletStatus).mockResolvedValue(sampleWalletStatus as never); + + render(); + await openAndSubmitCreateForm(user); + + await waitFor(() => { + expect(screen.getByText('Bounty created')).toBeInTheDocument(); + expect(screen.getByText('My new bounty')).toBeInTheDocument(); + expect(screen.getByText('View')).toBeInTheDocument(); + }); + }); + + test('View action in toast expands the created bounty row', async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.bounties.create).mockResolvedValue({ + bounty: { ...sampleOwnBounty, status: 'open', title: 'My new bounty' }, + } as never); + // List returns the created bounty so the row exists to expand + vi.mocked(apiClient.bounties.list).mockResolvedValue({ + bounties: [{ ...sampleOwnBounty, status: 'open', title: 'My new bounty' }], + }); + vi.mocked(fetchWalletStatus).mockResolvedValue(sampleWalletStatus as never); + + render(); + await openAndSubmitCreateForm(user); + + // Wait for the toast to appear + await waitFor(() => { + expect(screen.getByText('View')).toBeInTheDocument(); + }); + + // Click "View" — should expand the bounty row (description becomes visible) + await user.click(screen.getByText('View')); + + await waitFor(() => { + expect( + screen.getByText('Create a TypeScript plugin that connects to our API.') + ).toBeInTheDocument(); + }); + }); + + test('list refetches after successful creation', async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.bounties.create).mockResolvedValue({ + bounty: { ...sampleOwnBounty, status: 'open', title: 'My new bounty' }, + } as never); + vi.mocked(apiClient.bounties.list).mockResolvedValue(listWithOwnBounty); + vi.mocked(fetchWalletStatus).mockResolvedValue(sampleWalletStatus as never); + + render(); + + // list is called once on mount + await waitFor(() => { + expect(apiClient.bounties.list).toHaveBeenCalledTimes(1); + }); + + await openAndSubmitCreateForm(user); + + // list should be called a second time after create succeeds + await waitFor(() => { + expect(apiClient.bounties.list).toHaveBeenCalledTimes(2); + }); + }); + + test('toast can be dismissed', async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.bounties.create).mockResolvedValue({ + bounty: { ...sampleOwnBounty, status: 'open', title: 'My new bounty' }, + } as never); + vi.mocked(apiClient.bounties.list).mockResolvedValue(listWithOwnBounty); + vi.mocked(fetchWalletStatus).mockResolvedValue(sampleWalletStatus as never); + + render(); + await openAndSubmitCreateForm(user); + + await waitFor(() => { + expect(screen.getByText('Bounty created')).toBeInTheDocument(); + }); + + // Find the close button inside the toast (svg close button, aria unlabelled) + // The Toast component renders a close