Skip to content

Commit 1a636c1

Browse files
Copilothotlong
andcommitted
Add tests for useActionRunner hook
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent b6d0c82 commit 1a636c1

1 file changed

Lines changed: 153 additions & 0 deletions

File tree

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* Tests for useActionRunner hook
3+
*/
4+
5+
import { describe, it, expect, vi } from 'vitest';
6+
import { renderHook, waitFor, act } from '@testing-library/react';
7+
import { useActionRunner } from '../useActionRunner';
8+
9+
describe('useActionRunner', () => {
10+
it('returns initial state with loading=false, error=null, result=null', () => {
11+
const { result } = renderHook(() => useActionRunner());
12+
13+
expect(result.current.loading).toBe(false);
14+
expect(result.current.error).toBeNull();
15+
expect(result.current.result).toBeNull();
16+
});
17+
18+
it('exposes runner, execute, and updateContext', () => {
19+
const { result } = renderHook(() => useActionRunner());
20+
21+
expect(result.current.runner).toBeDefined();
22+
expect(typeof result.current.execute).toBe('function');
23+
expect(typeof result.current.updateContext).toBe('function');
24+
});
25+
26+
it('handles options with context key format', () => {
27+
const { result } = renderHook(() =>
28+
useActionRunner({ context: { data: { name: 'test' } } }),
29+
);
30+
31+
expect(result.current.runner).toBeDefined();
32+
expect(result.current.loading).toBe(false);
33+
});
34+
35+
it('handles plain context object format (backwards compat)', () => {
36+
const { result } = renderHook(() =>
37+
useActionRunner({ data: { name: 'test' } }),
38+
);
39+
40+
expect(result.current.runner).toBeDefined();
41+
expect(result.current.loading).toBe(false);
42+
});
43+
44+
it('sets loading=true during execution and loading=false after', async () => {
45+
let resolveAction: () => void;
46+
const pendingAction = new Promise<void>((resolve) => {
47+
resolveAction = resolve;
48+
});
49+
50+
const { result } = renderHook(() => useActionRunner());
51+
52+
let executePromise: Promise<unknown>;
53+
act(() => {
54+
executePromise = result.current.execute({
55+
onClick: () => pendingAction,
56+
});
57+
});
58+
59+
await waitFor(() => {
60+
expect(result.current.loading).toBe(true);
61+
});
62+
63+
await act(async () => {
64+
resolveAction!();
65+
await executePromise;
66+
});
67+
68+
expect(result.current.loading).toBe(false);
69+
});
70+
71+
it('sets result on successful action execution', async () => {
72+
const onNavigate = vi.fn();
73+
const { result } = renderHook(() =>
74+
useActionRunner({ context: {}, onNavigate }),
75+
);
76+
77+
await act(async () => {
78+
await result.current.execute({
79+
type: 'navigation',
80+
navigate: { to: '/dashboard' },
81+
});
82+
});
83+
84+
expect(result.current.result).toEqual({ success: true });
85+
expect(result.current.error).toBeNull();
86+
expect(result.current.loading).toBe(false);
87+
expect(onNavigate).toHaveBeenCalledWith(
88+
'/dashboard',
89+
expect.objectContaining({ external: false }),
90+
);
91+
});
92+
93+
it('sets error on failed action execution (catch path)', async () => {
94+
const { result } = renderHook(() => useActionRunner());
95+
96+
await act(async () => {
97+
await result.current.execute({
98+
onClick: () => {
99+
throw new Error('Something went wrong');
100+
},
101+
});
102+
});
103+
104+
expect(result.current.error).toBe('Something went wrong');
105+
expect(result.current.result).toEqual({
106+
success: false,
107+
error: 'Something went wrong',
108+
});
109+
expect(result.current.loading).toBe(false);
110+
});
111+
112+
it('updateContext calls runner.updateContext', () => {
113+
const { result } = renderHook(() => useActionRunner());
114+
115+
const spy = vi.spyOn(result.current.runner, 'updateContext');
116+
117+
act(() => {
118+
result.current.updateContext({ data: { id: 42 } });
119+
});
120+
121+
expect(spy).toHaveBeenCalledWith({ data: { id: 42 } });
122+
});
123+
124+
it('accepts handler options (onToast, onNavigate, etc.)', async () => {
125+
const onToast = vi.fn();
126+
const onNavigate = vi.fn();
127+
128+
const { result } = renderHook(() =>
129+
useActionRunner({
130+
context: {},
131+
onToast,
132+
onNavigate,
133+
}),
134+
);
135+
136+
await act(async () => {
137+
await result.current.execute({
138+
type: 'navigation',
139+
navigate: { to: '/settings' },
140+
successMessage: 'Navigated!',
141+
});
142+
});
143+
144+
expect(onNavigate).toHaveBeenCalledWith(
145+
'/settings',
146+
expect.objectContaining({ external: false }),
147+
);
148+
expect(onToast).toHaveBeenCalledWith(
149+
'Navigated!',
150+
expect.objectContaining({ type: 'success' }),
151+
);
152+
});
153+
});

0 commit comments

Comments
 (0)