Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion dashboard/src/components/EventExplorerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ interface EventExplorerCardProps {
event: BlockchainEvent;
onCopyContract: (contractAddress: string) => void;
isCopied: boolean;
onSelect?: (event: BlockchainEvent) => void;
}

export function EventExplorerCard({ event, onCopyContract, isCopied, onSelect }: EventExplorerCardProps) {
contractStatuses: ContractStatus[];
}

Expand All @@ -50,12 +54,40 @@ export function EventExplorerCard({
const kindLabel = getEventKindLabel(event.type);

return (
<article className="event-explorer__row" role="row" data-event-id={event.eventId}>
<article
className={`event-explorer__row${onSelect ? ' event-card--clickable' : ''}`}
role={onSelect ? 'button' : 'row'}
tabIndex={onSelect ? 0 : undefined}
data-event-id={event.eventId}
onClick={onSelect ? () => onSelect(event) : undefined}
onKeyDown={
onSelect
? (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onSelect(event);
}
}
: undefined
}
aria-label={onSelect ? `View details for ${label} notification` : undefined}
>
<div className="event-explorer__cell" data-label="Contract" role="cell">
<div>
<p className="event-explorer__contract" title={event.contractAddress}>
{shortenAddress(event.contractAddress)}
</p>
<button
type="button"
className="event-explorer__copy-button"
onClick={(e) => {
e.stopPropagation();
onCopyContract(event.contractAddress);
}}
aria-label={`Copy contract address ${event.contractAddress}`}
>
{isCopied ? 'Copied' : 'Copy'}
</button>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button
type="button"
Expand Down
5 changes: 5 additions & 0 deletions dashboard/src/components/EventExplorerTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { EventExplorerCard } from './EventExplorerCard';

interface EventExplorerTableProps {
events: BlockchainEvent[];
onSelectEvent?: (event: BlockchainEvent) => void;
}

export function EventExplorerTable({ events, onSelectEvent }: EventExplorerTableProps) {
contractStatuses: ContractStatus[];
}

Expand Down Expand Up @@ -60,6 +64,7 @@ export function EventExplorerTable({ events, contractStatuses }: EventExplorerTa
event={event}
onCopyContract={handleCopyContract}
isCopied={copiedAddress === event.contractAddress}
onSelect={onSelectEvent}
contractStatuses={contractStatuses}
/>
))}
Expand Down
111 changes: 111 additions & 0 deletions dashboard/src/components/NotificationDetailsDrawer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import '@testing-library/jest-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import { NotificationDetailsDrawer } from './NotificationDetailsDrawer';
import type { BlockchainEvent } from '../types/event';

function makeNotification(overrides: Partial<BlockchainEvent> = {}): BlockchainEvent {
return {
eventId: 'evt-1',
contractAddress: 'GABCDEF',
eventName: 'TaskCreated',
ledger: 123,
type: 'contract',
topic: [],
value: '42',
txHash: 'TXHASH123',
receivedAt: Date.now(),
...overrides,
};
}

describe('NotificationDetailsDrawer', () => {
it('mounts and renders core notification metadata', async () => {
const notification = makeNotification();
const onClose = jest.fn();

render(
<NotificationDetailsDrawer
isOpen={true}
notification={notification}
onClose={onClose}
/>
);

expect(screen.getByRole('dialog', { name: 'Notification details' })).toBeInTheDocument();
expect(screen.getByText('Sender Details')).toBeInTheDocument();
expect(screen.getByText('Blockchain Context')).toBeInTheDocument();
expect(screen.getByText('Notification Status History')).toBeInTheDocument();

expect(screen.getByText('Ledger')).toBeInTheDocument();
expect(screen.getByText('123')).toBeInTheDocument();
expect(screen.getByText('TXHASH123')).toBeInTheDocument();
});

it('calls onClose when the close button is clicked', () => {
const notification = makeNotification();
const onClose = jest.fn();

render(
<NotificationDetailsDrawer
isOpen={true}
notification={notification}
onClose={onClose}
/>
);

fireEvent.click(screen.getByRole('button', { name: 'Close drawer' }));
expect(onClose).toHaveBeenCalledTimes(1);
});

it('renders status history from metadata and guards clipboard exceptions', async () => {
const notification = makeNotification({ contractAddress: 'GABCDEF', txHash: 'TXHASH123' });
const onClose = jest.fn();

Object.assign(navigator, {
clipboard: {
writeText: jest.fn().mockRejectedValue(new Error('Denied')),
},
});

render(
<NotificationDetailsDrawer
isOpen={true}
notification={notification}
onClose={onClose}
fetchMetadata={async () => ({
sender: { address: 'GABCDEF', metadata: { note: 'unit-test' } },
statusHistory: [
{ label: 'Queued', timestampMs: 1, detail: 'Enqueued for delivery' },
{ label: 'Delivered', timestampMs: 2, detail: 'Sent successfully' },
],
})}
/>
);

expect(await screen.findByText('Queued')).toBeInTheDocument();
expect(await screen.findByText('Delivered')).toBeInTheDocument();
expect(await screen.findByText('unit-test')).toBeInTheDocument();

const txCopyButtons = screen.getAllByRole('button', { name: 'Copy' });
fireEvent.click(txCopyButtons[txCopyButtons.length - 1]);
expect(await screen.findByText('Copy failed')).toBeInTheDocument();
});

it('shows an error fallback when metadata fetch fails', async () => {
const notification = makeNotification();

render(
<NotificationDetailsDrawer
isOpen={true}
notification={notification}
onClose={() => {}}
fetchMetadata={async () => {
throw new Error('boom');
}}
/>
);

expect(await screen.findByText(/Failed to load details: boom/i)).toBeInTheDocument();
});
});

Loading
Loading