mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(agent-world): confirm bounty creation with a success toast (#3837)
This commit is contained in:
@@ -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<typeof userEvent.setup>) {
|
||||
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(<BountiesSection />);
|
||||
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(<BountiesSection />);
|
||||
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(<BountiesSection />);
|
||||
|
||||
// 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(<BountiesSection />);
|
||||
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 <button> after the action button.
|
||||
// We locate it by finding all buttons visible and clicking the one without
|
||||
// meaningful text content that follows the "View" action button.
|
||||
const closeBtn = screen.getAllByRole('button').find(btn => btn.textContent?.trim() === '');
|
||||
expect(closeBtn).toBeDefined();
|
||||
await user.click(closeBtn!);
|
||||
|
||||
// After dismiss animation (200ms) the toast is removed; but in jsdom
|
||||
// setTimeout fires synchronously via fake timers — use waitFor instead.
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.queryByText('Bounty created')).not.toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 1000 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Submissions section ───────────────────────────────────────────────────────
|
||||
|
||||
describe('Submissions section', () => {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
*/
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { ToastContainer } from '../../components/intelligence/Toast';
|
||||
import PanelScaffold from '../../components/layout/PanelScaffold';
|
||||
import Button from '../../components/ui/Button';
|
||||
import { ModalShell } from '../../components/ui/ModalShell';
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
type RegistryWalletBalance,
|
||||
} from '../../lib/agentworld/invokeApiClient';
|
||||
import { fetchWalletStatus } from '../../services/walletApi';
|
||||
import type { ToastNotification } from '../../types/intelligence';
|
||||
import { apiClient } from '../AgentWorldShell';
|
||||
import X402ConfirmDialog, { formatUnits } from '../components/X402ConfirmDialog';
|
||||
import { useX402Buy } from '../hooks/useX402Buy';
|
||||
@@ -867,6 +869,12 @@ export default function BountiesSection() {
|
||||
const [expandedBountyId, setExpandedBountyId] = useState<string | null>(null);
|
||||
const [mutating, setMutating] = useState(false);
|
||||
|
||||
// Toast state
|
||||
const [toasts, setToasts] = useState<ToastNotification[]>([]);
|
||||
const addToast = (toast: Omit<ToastNotification, 'id'>) =>
|
||||
setToasts(prev => [...prev, { ...toast, id: crypto.randomUUID() }]);
|
||||
const removeToast = (id: string) => setToasts(prev => prev.filter(t => t.id !== id));
|
||||
|
||||
// Modal state
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [submitWorkBountyId, setSubmitWorkBountyId] = useState<string | null>(null);
|
||||
@@ -997,6 +1005,20 @@ export default function BountiesSection() {
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
onCreated={bounty => {
|
||||
setShowCreateModal(false);
|
||||
// Show success toast with a "View" action that expands the new row.
|
||||
// The expand state is pre-set here; once the list refetch completes
|
||||
// the row auto-expands. No i18n yet — the entire BountiesSection
|
||||
// uses hardcoded English strings (TODO: i18n when section is
|
||||
// internationalised).
|
||||
addToast({
|
||||
type: 'success',
|
||||
title: 'Bounty created',
|
||||
message: (bounty as Bounty).title,
|
||||
action: {
|
||||
label: 'View',
|
||||
handler: () => setExpandedBountyId((bounty as Bounty).bountyId),
|
||||
},
|
||||
});
|
||||
fetchBounties();
|
||||
// If the new bounty is a draft, offer to fund it
|
||||
if ((bounty as Bounty).status === 'draft') {
|
||||
@@ -1130,6 +1152,8 @@ export default function BountiesSection() {
|
||||
</div>
|
||||
</ModalShell>
|
||||
)}
|
||||
|
||||
<ToastContainer notifications={toasts} onRemove={removeToast} />
|
||||
</PanelScaffold>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user