+
+
+
+
+
+ {#each secondary as action (action.label)}
+
+ {/each}
+
+
+
+ {#if secondary.length > 0}
+
+
+
+ {#if menuOpen}
+
e.stopPropagation()}
+ onkeydown={(e) => e.stopPropagation()}
+ >
+ {#each secondary as action (action.label)}
+ fire(action)}
+ danger={action.danger}
+ disabled={action.disabled}
+ />
+ {/each}
+
+ {/if}
+
+ {/if}
+
diff --git a/web/src/lib/components/RowActionsMenu.test.ts b/web/src/lib/components/RowActionsMenu.test.ts
new file mode 100644
index 00000000..53696512
--- /dev/null
+++ b/web/src/lib/components/RowActionsMenu.test.ts
@@ -0,0 +1,70 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/svelte';
+import { Check, X, Trash2 } from 'lucide-svelte';
+import RowActionsMenu from './RowActionsMenu.svelte';
+
+describe('RowActionsMenu', () => {
+ it('renders the primary action inline', () => {
+ const onApprove = vi.fn();
+ render(RowActionsMenu, {
+ props: {
+ primary: { icon: Check, label: 'Approve', onclick: onApprove },
+ secondary: []
+ }
+ });
+ expect(screen.getByText('Approve')).toBeInTheDocument();
+ });
+
+ it('fires the primary action onclick', async () => {
+ const onApprove = vi.fn();
+ render(RowActionsMenu, {
+ props: {
+ primary: { icon: Check, label: 'Approve', onclick: onApprove },
+ secondary: []
+ }
+ });
+ await fireEvent.click(screen.getByText('Approve'));
+ expect(onApprove).toHaveBeenCalledOnce();
+ });
+
+ it('renders secondary action labels and exposes the overflow trigger', () => {
+ render(RowActionsMenu, {
+ props: {
+ primary: { icon: Check, label: 'Approve', onclick: vi.fn() },
+ secondary: [
+ { icon: X, label: 'Reassign', onclick: vi.fn() },
+ { icon: Trash2, label: 'Delete', onclick: vi.fn(), danger: true }
+ ]
+ }
+ });
+ // Both inline (md:flex) and overflow-trigger (md:hidden) markup are
+ // present in jsdom; the test confirms labels exist and the overflow
+ // trigger is in the DOM.
+ expect(screen.getAllByText('Reassign').length).toBeGreaterThan(0);
+ expect(screen.getAllByText('Delete').length).toBeGreaterThan(0);
+ expect(screen.getByLabelText('More actions')).toBeInTheDocument();
+ });
+
+ it('opens the overflow menu when ⋮ is clicked', async () => {
+ render(RowActionsMenu, {
+ props: {
+ primary: { icon: Check, label: 'Approve', onclick: vi.fn() },
+ secondary: [{ icon: X, label: 'Reassign', onclick: vi.fn() }]
+ }
+ });
+ const trigger = screen.getByLabelText('More actions');
+ expect(trigger.getAttribute('aria-expanded')).toBe('false');
+ await fireEvent.click(trigger);
+ expect(trigger.getAttribute('aria-expanded')).toBe('true');
+ });
+
+ it('omits the overflow trigger when secondary is empty', () => {
+ render(RowActionsMenu, {
+ props: {
+ primary: { icon: Check, label: 'Approve', onclick: vi.fn() },
+ secondary: []
+ }
+ });
+ expect(screen.queryByLabelText('More actions')).not.toBeInTheDocument();
+ });
+});