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
40 changes: 40 additions & 0 deletions src/components/TransactionHistory.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,46 @@ describe("TransactionHistory", () => {
expect(row.tagName).toBe("DIV");
expect(row).not.toHaveAttribute("href");
});

it("mentions the explorer link in the row's aria-label when it is one (#563)", async () => {
vi.mocked(useSorokit).mockReturnValue(
mockUseSorokit({
address: ADDRESS,
isConnected: true,
network: { name: "testnet" } as ReturnType<typeof useSorokit>["network"],
}),
);
const tx = makeTx(0);
mockGetHistory([tx], 1);

render(<TransactionHistory />);
act(() => { vi.advanceTimersByTime(0); });

await waitFor(() => screen.getByRole("article"));
expect(screen.getByRole("article")).toHaveAccessibleName(
expect.stringMatching(/stellar expert.*opens in a new tab/i),
);
});

it("does not mention an explorer link in the aria-label for a non-link row (#563)", async () => {
vi.mocked(useSorokit).mockReturnValue(
mockUseSorokit({
address: ADDRESS,
isConnected: true,
network: { name: "futurenet" } as ReturnType<typeof useSorokit>["network"],
}),
);
const tx = makeTx(0);
mockGetHistory([tx], 1);

render(<TransactionHistory />);
act(() => { vi.advanceTimersByTime(0); });

await waitFor(() => screen.getByRole("article"));
expect(screen.getByRole("article")).not.toHaveAccessibleName(
expect.stringMatching(/stellar expert/i),
);
});
});

describe("status and date range filtering (#350, #352)", () => {
Expand Down
8 changes: 7 additions & 1 deletion src/components/TransactionHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,17 @@ export const TxRow = memo(function TxRow({
? { href: explorerUrl, target: "_blank", rel: "noopener noreferrer" }
: {};

const rowLabel = `Transaction ${truncateAddress(tx.hash, 10, 6)} — ${tx.successful ? "Success" : "Failed"} — Fee: ${tx.feePaid} stroops`;

return (
<RowWrapper
{...(wrapperProps as Record<string, string>)}
role="article"
aria-label={`Transaction ${truncateAddress(tx.hash, 10, 6)} — ${tx.successful ? "Success" : "Failed"} — Fee: ${tx.feePaid} stroops`}
aria-label={
explorerUrl
? `${rowLabel} — view on Stellar Expert (opens in a new tab)`
: rowLabel
}
className="flex items-center justify-between px-5 py-3.5 border-b border-line last:border-0 gap-4 hover:bg-surface-2 transition-colors cursor-pointer"
>
<div className="flex items-center gap-3 min-w-0">
Expand Down
33 changes: 33 additions & 0 deletions src/components/TransactionPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,39 @@ describe("TransactionPanel", () => {
"https://stellar.expert/explorer/testnet/tx/txhash123",
);
});

it("gives both explorer links an accessible aria-label (#563)", async () => {
const mockSubmit = vi
.fn()
.mockResolvedValue({ data: { hash: "txhash123", ledger: 100 }, error: null });
mockGetClient(mockSubmit);
vi.mocked(useSorokit).mockReturnValue({
address: "GABC",
isConnected: true,
network: { name: "testnet", passphrase: "x", rpcUrl: "x", horizonUrl: "x" },
client: getClient(),
} as unknown as ReturnType<typeof useSorokit>);

render(<TransactionPanel />);

const validDest = "GCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC";
fireEvent.change(screen.getByLabelText("Destination Address"), { target: { value: validDest } });
fireEvent.change(screen.getByLabelText("Amount (XLM)"), { target: { value: "10" } });

await reviewAndConfirm();
await screen.findByText("Transaction submitted");

// Scoped to the two links this change touches — TransactionStatusTracker
// renders its own separate explorer link lower in the panel.
const hashLink = screen.getByText("txhash123").closest("a")!;
const badgeLink = screen.getByRole("link", {
name: /view on stellar expert/i,
});
for (const link of [hashLink, badgeLink]) {
expect(link).toHaveAccessibleName(expect.stringContaining("txhash123"));
expect(link).toHaveAccessibleName(expect.stringMatching(/opens in a new tab/i));
}
});
});

describe("default prop pre-fill (#351)", () => {
Expand Down
4 changes: 3 additions & 1 deletion src/components/TransactionPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,10 @@ export function TransactionPanel({
href={explorerUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={`View transaction ${result.hash} on Stellar Expert (opens in a new tab)`}
className="break-all leading-relaxed text-brand hover:underline inline-flex items-start gap-1.5"
>
<span>{result.hash}</span>
<span aria-hidden="true">{result.hash}</span>
<ExternalLinkIcon className="mt-[3px] shrink-0 opacity-70" />
</a>
) : (
Expand All @@ -287,6 +288,7 @@ export function TransactionPanel({
href={explorerUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={`View on Stellar Expert: transaction ${result.hash} (opens in a new tab)`}
className="text-[11px] text-brand hover:underline"
>
View on Stellar Expert
Expand Down