Skip to content
Open
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
56 changes: 56 additions & 0 deletions frontend/src/components/StreamDetailDrawer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -403,3 +403,59 @@ describe('StreamDetailDrawer', () => {
});
});
});

// ── Stellar Expert transaction links (#399) ──────────────────────────────────

describe('TxHashLink — Stellar Expert transaction links (#399)', () => {
beforeEach(() => {
onClose.mockClear();
clearCache();
});

it('renders txHash as a link to Stellar Expert testnet', async () => {
render(<StreamDetailDrawer streamId="42" onClose={onClose} />);
await waitFor(() => expect(screen.getByText('Tokens claimed')).toBeInTheDocument());

const link = screen.getByRole('link', { name: /View transaction.*Stellar Expert/i });
expect(link).toHaveAttribute(
'href',
'https://stellar.expert/explorer/testnet/tx/abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab',
);
});

it('link opens in a new tab with rel=noopener noreferrer', async () => {
render(<StreamDetailDrawer streamId="42" onClose={onClose} />);
await waitFor(() => expect(screen.getByText('Tokens claimed')).toBeInTheDocument());

const link = screen.getByRole('link', { name: /View transaction/i });
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
});

it('displays truncated hash (first 8 + last 8 chars)', async () => {
render(<StreamDetailDrawer streamId="42" onClose={onClose} />);
await waitFor(() => expect(screen.getByText('Tokens claimed')).toBeInTheDocument());

// full: abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab
expect(screen.getByText('abcdef12\u2026567890ab')).toBeInTheDocument();
});

it('full hash is in the tooltip (title attribute)', async () => {
render(<StreamDetailDrawer streamId="42" onClose={onClose} />);
await waitFor(() => expect(screen.getByText('Tokens claimed')).toBeInTheDocument());

const link = screen.getByRole('link', { name: /View transaction/i });
expect(link).toHaveAttribute(
'title',
'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab',
);
});

it('does not render a tx link when txHash is absent', async () => {
render(<StreamDetailDrawer streamId="42" onClose={onClose} />);
await waitFor(() => expect(screen.getByText('Stream created')).toBeInTheDocument());
// "created" event has no txHash — only the "claimed" event has a link
const links = screen.queryAllByRole('link', { name: /View transaction/i });
expect(links.length).toBeLessThanOrEqual(1);
});
Comment on lines +454 to +460

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert against the specific "Stream created" event row.

Line 458 only caps the total number of tx links on the page, so this still passes if the link moves from the claimed event to the created event. Scope the assertion to the "Stream created" list item and verify that row has no transaction link.

Suggested test shape
   it('does not render a tx link when txHash is absent', async () => {
     render(<StreamDetailDrawer streamId="42" onClose={onClose} />);
     await waitFor(() => expect(screen.getByText('Stream created')).toBeInTheDocument());
-    // "created" event has no txHash — only the "claimed" event has a link
-    const links = screen.queryAllByRole('link', { name: /View transaction/i });
-    expect(links.length).toBeLessThanOrEqual(1);
+    const createdRow = screen.getByText('Stream created').closest('li');
+    expect(createdRow).not.toBeNull();
+    expect(
+      within(createdRow as HTMLElement).queryByRole('link', { name: /View transaction/i }),
+    ).not.toBeInTheDocument();
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('does not render a tx link when txHash is absent', async () => {
render(<StreamDetailDrawer streamId="42" onClose={onClose} />);
await waitFor(() => expect(screen.getByText('Stream created')).toBeInTheDocument());
// "created" event has no txHash — only the "claimed" event has a link
const links = screen.queryAllByRole('link', { name: /View transaction/i });
expect(links.length).toBeLessThanOrEqual(1);
});
it('does not render a tx link when txHash is absent', async () => {
render(<StreamDetailDrawer streamId="42" onClose={onClose} />);
await waitFor(() => expect(screen.getByText('Stream created')).toBeInTheDocument());
const createdRow = screen.getByText('Stream created').closest('li');
expect(createdRow).not.toBeNull();
expect(
within(createdRow as HTMLElement).queryByRole('link', { name: /View transaction/i }),
).not.toBeInTheDocument();
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/StreamDetailDrawer.test.tsx` around lines 454 - 460,
The test in StreamDetailDrawer is too broad because it only checks the total
number of “View transaction” links, so it won’t catch a link appearing on the
“Stream created” row. Update the assertion in the test case for
StreamDetailDrawer to scope directly to the “Stream created” list item/row and
verify that specific row has no transaction link, while still allowing the
claimed event to have one. Use the existing StreamDetailDrawer render and the
“Stream created” text to locate the correct row.

});
25 changes: 25 additions & 0 deletions frontend/src/components/StreamDetailDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@ import { Stream } from "../types/stream";
import { StreamEvent, getStream, getStreamHistory } from "../services/api";
import { CopyableAddress } from "./CopyableAddress";

const STELLAR_EXPERT_BASE = "https://stellar.expert/explorer/testnet/tx";

/**
* Renders a transaction hash as a truncated, clickable link to Stellar Expert.
* Shows first 8 + last 8 characters with the full hash in a tooltip.
*/
export function TxHashLink({ txHash }: { txHash: string }) {
const truncated = `${txHash.slice(0, 8)}…${txHash.slice(-8)}`;
return (
<a
href={`${STELLAR_EXPERT_BASE}/${txHash}`}
target="_blank"
rel="noopener noreferrer"
title={txHash}
className="tx-hash-link"
aria-label={`View transaction ${txHash} on Stellar Expert`}
>
<code>{truncated}</code>
</a>
);
}

interface StreamDetailDrawerProps {
streamId: string;
/** Called when the drawer should close */
Expand Down Expand Up @@ -472,6 +494,9 @@ export function StreamDetailDrawer({
{evt.amount != null && (
<span>· {evt.amount} tokens</span>
)}
{evt.txHash && (
<span>· <TxHashLink txHash={evt.txHash} /></span>
)}
</div>
</div>
</li>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const handlers = [
timestamp: 1700010000,
actor: 'GRECIPIENT456',
amount: 100,
txHash: 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab',
},
],
});
Expand Down
1 change: 1 addition & 0 deletions frontend/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ export interface StreamEvent {
timestamp: number;
actor?: string;
amount?: number;
txHash?: string;
metadata?: Record<string, any>;
}

Expand Down