@workos-inc/authkit-nextjs 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +124 -29
  2. package/dist/esm/components/tokenStore.js +110 -11
  3. package/dist/esm/components/tokenStore.js.map +1 -1
  4. package/dist/esm/components/useAccessToken.js +6 -1
  5. package/dist/esm/components/useAccessToken.js.map +1 -1
  6. package/dist/esm/cookie.js +51 -0
  7. package/dist/esm/cookie.js.map +1 -1
  8. package/dist/esm/middleware.js +2 -2
  9. package/dist/esm/middleware.js.map +1 -1
  10. package/dist/esm/session.js +35 -2
  11. package/dist/esm/session.js.map +1 -1
  12. package/dist/esm/test-helpers.js +57 -0
  13. package/dist/esm/test-helpers.js.map +1 -0
  14. package/dist/esm/types/components/tokenStore.d.ts +7 -2
  15. package/dist/esm/types/cookie.d.ts +1 -0
  16. package/dist/esm/types/interfaces.d.ts +2 -0
  17. package/dist/esm/types/middleware.d.ts +1 -1
  18. package/dist/esm/types/session.d.ts +1 -1
  19. package/dist/esm/types/test-helpers.d.ts +3 -0
  20. package/dist/esm/types/workos.d.ts +1 -1
  21. package/dist/esm/workos.js +1 -1
  22. package/package.json +4 -3
  23. package/src/actions.spec.ts +100 -0
  24. package/src/auth.spec.ts +347 -0
  25. package/src/authkit-callback-route.spec.ts +258 -0
  26. package/src/components/authkit-provider.spec.tsx +471 -0
  27. package/src/components/button.spec.tsx +46 -0
  28. package/src/components/impersonation.spec.tsx +134 -0
  29. package/src/components/min-max-button.spec.tsx +60 -0
  30. package/src/components/tokenStore.spec.ts +816 -0
  31. package/src/components/tokenStore.ts +147 -12
  32. package/src/components/useAccessToken.spec.tsx +731 -0
  33. package/src/components/useAccessToken.ts +6 -1
  34. package/src/components/useTokenClaims.spec.tsx +194 -0
  35. package/src/cookie.spec.ts +276 -0
  36. package/src/cookie.ts +56 -0
  37. package/src/get-authorization-url.spec.ts +60 -0
  38. package/src/interfaces.ts +2 -0
  39. package/src/jwt.spec.ts +159 -0
  40. package/src/middleware.ts +2 -1
  41. package/src/session.spec.ts +1152 -0
  42. package/src/session.ts +41 -1
  43. package/src/test-helpers.ts +70 -0
  44. package/src/utils.spec.ts +142 -0
  45. package/src/workos.spec.ts +67 -0
  46. package/src/workos.ts +1 -1
@@ -0,0 +1,471 @@
1
+ import React from 'react';
2
+ import { render, waitFor, act } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { AuthKitProvider, useAuth } from './authkit-provider.js';
5
+ import {
6
+ checkSessionAction,
7
+ getAuthAction,
8
+ refreshAuthAction,
9
+ handleSignOutAction,
10
+ switchToOrganizationAction,
11
+ } from '../actions.js';
12
+
13
+ jest.mock('../actions', () => ({
14
+ checkSessionAction: jest.fn(),
15
+ getAuthAction: jest.fn(),
16
+ refreshAuthAction: jest.fn(),
17
+ handleSignOutAction: jest.fn(),
18
+ switchToOrganizationAction: jest.fn(),
19
+ }));
20
+
21
+ describe('AuthKitProvider', () => {
22
+ beforeEach(() => {
23
+ jest.clearAllMocks();
24
+ });
25
+
26
+ it('should render children', async () => {
27
+ const { getByText } = await act(async () => {
28
+ return render(
29
+ <AuthKitProvider>
30
+ <div>Test Child</div>
31
+ </AuthKitProvider>,
32
+ );
33
+ });
34
+
35
+ expect(getByText('Test Child')).toBeInTheDocument();
36
+ });
37
+
38
+ it('should do nothing if onSessionExpired is false', async () => {
39
+ jest.spyOn(window, 'addEventListener');
40
+
41
+ await act(async () => {
42
+ render(
43
+ <AuthKitProvider onSessionExpired={false}>
44
+ <div>Test Child</div>
45
+ </AuthKitProvider>,
46
+ );
47
+ });
48
+
49
+ // expect window to not have an event listener
50
+ expect(window.addEventListener).not.toHaveBeenCalled();
51
+ });
52
+
53
+ it('should call onSessionExpired when session is expired', async () => {
54
+ (checkSessionAction as jest.Mock).mockRejectedValueOnce(new Error('Failed to fetch'));
55
+ const onSessionExpired = jest.fn();
56
+
57
+ render(
58
+ <AuthKitProvider onSessionExpired={onSessionExpired}>
59
+ <div>Test Child</div>
60
+ </AuthKitProvider>,
61
+ );
62
+
63
+ act(() => {
64
+ // Simulate visibility change
65
+ window.dispatchEvent(new Event('visibilitychange'));
66
+ });
67
+
68
+ await waitFor(() => {
69
+ expect(onSessionExpired).toHaveBeenCalled();
70
+ });
71
+ });
72
+
73
+ it('should only call onSessionExpired once if multiple visibility changes occur', async () => {
74
+ (checkSessionAction as jest.Mock).mockRejectedValueOnce(new Error('Failed to fetch'));
75
+ const onSessionExpired = jest.fn();
76
+
77
+ render(
78
+ <AuthKitProvider onSessionExpired={onSessionExpired}>
79
+ <div>Test Child</div>
80
+ </AuthKitProvider>,
81
+ );
82
+
83
+ act(() => {
84
+ // Simulate visibility change twice
85
+ window.dispatchEvent(new Event('visibilitychange'));
86
+ window.dispatchEvent(new Event('visibilitychange'));
87
+ });
88
+
89
+ await waitFor(() => {
90
+ expect(onSessionExpired).toHaveBeenCalledTimes(1);
91
+ });
92
+ });
93
+
94
+ it('should pass through if checkSessionAction does not throw "Failed to fetch"', async () => {
95
+ (checkSessionAction as jest.Mock).mockResolvedValueOnce(false);
96
+
97
+ const onSessionExpired = jest.fn();
98
+
99
+ render(
100
+ <AuthKitProvider onSessionExpired={onSessionExpired}>
101
+ <div>Test Child</div>
102
+ </AuthKitProvider>,
103
+ );
104
+
105
+ act(() => {
106
+ // Simulate visibility change
107
+ window.dispatchEvent(new Event('visibilitychange'));
108
+ });
109
+
110
+ await waitFor(() => {
111
+ expect(onSessionExpired).not.toHaveBeenCalled();
112
+ });
113
+ });
114
+
115
+ it('should reload the page when session is expired and no onSessionExpired handler is provided', async () => {
116
+ (checkSessionAction as jest.Mock).mockRejectedValueOnce(new Error('Failed to fetch'));
117
+
118
+ const originalLocation = window.location;
119
+
120
+ // @ts-expect-error - we're deleting the property to test the mock
121
+ delete window.location;
122
+
123
+ window.location = { ...window.location, reload: jest.fn() };
124
+
125
+ render(
126
+ <AuthKitProvider>
127
+ <div>Test Child</div>
128
+ </AuthKitProvider>,
129
+ );
130
+
131
+ act(() => {
132
+ // Simulate visibility change
133
+ window.dispatchEvent(new Event('visibilitychange'));
134
+ });
135
+
136
+ await waitFor(() => {
137
+ expect(window.location.reload).toHaveBeenCalled();
138
+ });
139
+
140
+ // Restore original reload function
141
+ window.location = originalLocation;
142
+ });
143
+
144
+ it('should not call onSessionExpired or reload the page if session is valid', async () => {
145
+ (checkSessionAction as jest.Mock).mockResolvedValueOnce(true);
146
+ const onSessionExpired = jest.fn();
147
+
148
+ const originalLocation = window.location;
149
+
150
+ // @ts-expect-error - we're deleting the property to test the mock
151
+ delete window.location;
152
+
153
+ window.location = { ...window.location, reload: jest.fn() };
154
+
155
+ render(
156
+ <AuthKitProvider onSessionExpired={onSessionExpired}>
157
+ <div>Test Child</div>
158
+ </AuthKitProvider>,
159
+ );
160
+
161
+ act(() => {
162
+ // Simulate visibility change
163
+ window.dispatchEvent(new Event('visibilitychange'));
164
+ });
165
+
166
+ await waitFor(() => {
167
+ expect(onSessionExpired).not.toHaveBeenCalled();
168
+ expect(window.location.reload).not.toHaveBeenCalled();
169
+ });
170
+
171
+ window.location = originalLocation;
172
+ });
173
+ });
174
+
175
+ describe('useAuth', () => {
176
+ beforeEach(() => {
177
+ jest.clearAllMocks();
178
+ });
179
+
180
+ it('should call getAuth when a user is not returned when ensureSignedIn is true', async () => {
181
+ // First and second calls return no user, second call returns a user
182
+ (getAuthAction as jest.Mock)
183
+ .mockResolvedValueOnce({ user: null, loading: true })
184
+ .mockResolvedValueOnce({ user: { email: 'test@example.com' }, loading: false });
185
+
186
+ const TestComponent = () => {
187
+ const auth = useAuth({ ensureSignedIn: true });
188
+ return <div data-testid="email">{auth.user?.email}</div>;
189
+ };
190
+
191
+ const { getByTestId } = render(
192
+ <AuthKitProvider>
193
+ <TestComponent />
194
+ </AuthKitProvider>,
195
+ );
196
+
197
+ await waitFor(() => {
198
+ expect(getAuthAction).toHaveBeenCalledTimes(2);
199
+ expect(getAuthAction).toHaveBeenLastCalledWith({ ensureSignedIn: true });
200
+ expect(getByTestId('email')).toHaveTextContent('test@example.com');
201
+ });
202
+ });
203
+
204
+ it('should throw error when used outside of AuthKitProvider', () => {
205
+ const TestComponent = () => {
206
+ const auth = useAuth();
207
+ return <div>{auth.user?.email}</div>;
208
+ };
209
+
210
+ // Suppress console.error for this test since we expect an error
211
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
212
+
213
+ expect(() => {
214
+ render(<TestComponent />);
215
+ }).toThrow('useAuth must be used within an AuthKitProvider');
216
+
217
+ consoleSpy.mockRestore();
218
+ });
219
+
220
+ it('should provide auth context values when used within AuthKitProvider', async () => {
221
+ (getAuthAction as jest.Mock).mockResolvedValueOnce({
222
+ user: { email: 'test@example.com' },
223
+ sessionId: 'test-session',
224
+ organizationId: 'test-org',
225
+ role: 'admin',
226
+ permissions: ['read', 'write'],
227
+ entitlements: ['feature1'],
228
+ featureFlags: ['test-flag'],
229
+ impersonator: { email: 'admin@example.com' },
230
+ });
231
+
232
+ const TestComponent = () => {
233
+ const auth = useAuth();
234
+ return (
235
+ <div>
236
+ <div data-testid="loading">{auth.loading.toString()}</div>
237
+ <div data-testid="email">{auth.user?.email}</div>
238
+ <div data-testid="session">{auth.sessionId}</div>
239
+ <div data-testid="org">{auth.organizationId}</div>
240
+ </div>
241
+ );
242
+ };
243
+
244
+ const { getByTestId } = render(
245
+ <AuthKitProvider>
246
+ <TestComponent />
247
+ </AuthKitProvider>,
248
+ );
249
+
250
+ // Initially loading
251
+ expect(getByTestId('loading')).toHaveTextContent('true');
252
+
253
+ // Wait for auth to load
254
+ await waitFor(() => {
255
+ expect(getByTestId('loading')).toHaveTextContent('false');
256
+ expect(getByTestId('email')).toHaveTextContent('test@example.com');
257
+ expect(getByTestId('session')).toHaveTextContent('test-session');
258
+ expect(getByTestId('org')).toHaveTextContent('test-org');
259
+ });
260
+ });
261
+
262
+ it('should handle auth methods (getAuth and refreshAuth)', async () => {
263
+ const mockAuth = {
264
+ user: { email: 'test@example.com' },
265
+ sessionId: 'test-session',
266
+ };
267
+
268
+ (getAuthAction as jest.Mock).mockResolvedValueOnce(mockAuth);
269
+ (refreshAuthAction as jest.Mock).mockResolvedValueOnce({
270
+ ...mockAuth,
271
+ sessionId: 'new-session',
272
+ });
273
+
274
+ const TestComponent = () => {
275
+ const auth = useAuth();
276
+ return (
277
+ <div>
278
+ <div data-testid="session">{auth.sessionId}</div>
279
+ <button onClick={() => auth.refreshAuth()}>Refresh</button>
280
+ </div>
281
+ );
282
+ };
283
+
284
+ const { getByTestId, getByRole } = render(
285
+ <AuthKitProvider>
286
+ <TestComponent />
287
+ </AuthKitProvider>,
288
+ );
289
+
290
+ await waitFor(() => {
291
+ expect(getByTestId('session')).toHaveTextContent('test-session');
292
+ });
293
+
294
+ // Test refresh
295
+ act(() => {
296
+ getByRole('button').click();
297
+ });
298
+
299
+ await waitFor(() => {
300
+ expect(getByTestId('session')).toHaveTextContent('new-session');
301
+ });
302
+ });
303
+
304
+ it('should handle switching organizations', async () => {
305
+ const mockAuth = {
306
+ user: { email: 'test@example.com' },
307
+ sessionId: 'test-session',
308
+ organizationId: 'new-org',
309
+ };
310
+
311
+ (getAuthAction as jest.Mock)
312
+ .mockResolvedValue(mockAuth)
313
+ .mockResolvedValueOnce({ ...mockAuth, organizationId: 'old-org' });
314
+ (switchToOrganizationAction as jest.Mock).mockResolvedValueOnce(mockAuth);
315
+
316
+ const TestComponent = () => {
317
+ const auth = useAuth();
318
+ return (
319
+ <div>
320
+ <div data-testid="org">{auth.organizationId}</div>
321
+ <button onClick={async () => await auth.switchToOrganization('test-org')}>Switch Organization</button>
322
+ </div>
323
+ );
324
+ };
325
+
326
+ const { getByTestId, getByRole } = render(
327
+ <AuthKitProvider>
328
+ <TestComponent />
329
+ </AuthKitProvider>,
330
+ );
331
+
332
+ await waitFor(() => {
333
+ expect(getByTestId('org')).toHaveTextContent('old-org');
334
+ });
335
+
336
+ // Test refresh
337
+ act(() => {
338
+ getByRole('button').click();
339
+ });
340
+
341
+ await waitFor(() => {
342
+ expect(getByTestId('org')).toHaveTextContent('new-org');
343
+ });
344
+ });
345
+
346
+ it('should receive an error when refreshAuth fails with an error', async () => {
347
+ (refreshAuthAction as jest.Mock).mockRejectedValueOnce(new Error('Refresh failed'));
348
+
349
+ let error: string | undefined;
350
+
351
+ const TestComponent = () => {
352
+ const auth = useAuth();
353
+ return (
354
+ <div>
355
+ <div data-testid="session">{auth.sessionId}</div>
356
+ <button
357
+ onClick={async () => {
358
+ const result = await auth.refreshAuth();
359
+ error = result?.error;
360
+ }}
361
+ >
362
+ Refresh
363
+ </button>
364
+ </div>
365
+ );
366
+ };
367
+
368
+ const { getByRole } = render(
369
+ <AuthKitProvider>
370
+ <TestComponent />
371
+ </AuthKitProvider>,
372
+ );
373
+
374
+ act(() => {
375
+ getByRole('button').click();
376
+ });
377
+
378
+ await waitFor(() => {
379
+ expect(error).toBe('Refresh failed');
380
+ });
381
+ });
382
+
383
+ it('should receive an error when refreshAuth fails with a string error', async () => {
384
+ (refreshAuthAction as jest.Mock).mockRejectedValueOnce('Refresh failed');
385
+
386
+ let error: string | undefined;
387
+
388
+ const TestComponent = () => {
389
+ const auth = useAuth();
390
+ return (
391
+ <div>
392
+ <div data-testid="session">{auth.sessionId}</div>
393
+ <button
394
+ onClick={async () => {
395
+ const result = await auth.refreshAuth();
396
+ error = result?.error;
397
+ }}
398
+ >
399
+ Refresh
400
+ </button>
401
+ </div>
402
+ );
403
+ };
404
+
405
+ const { getByRole } = render(
406
+ <AuthKitProvider>
407
+ <TestComponent />
408
+ </AuthKitProvider>,
409
+ );
410
+
411
+ act(() => {
412
+ getByRole('button').click();
413
+ });
414
+
415
+ await waitFor(() => {
416
+ expect(error).toBe('Refresh failed');
417
+ });
418
+ });
419
+
420
+ it('should call handleSignOutAction when signOut is called', async () => {
421
+ (handleSignOutAction as jest.Mock).mockResolvedValueOnce({});
422
+
423
+ const TestComponent = () => {
424
+ const auth = useAuth();
425
+ return (
426
+ <div>
427
+ <div data-testid="session">{auth.sessionId}</div>
428
+ <button onClick={() => auth.signOut()}>Sign out</button>
429
+ </div>
430
+ );
431
+ };
432
+
433
+ const { getByRole } = render(
434
+ <AuthKitProvider>
435
+ <TestComponent />
436
+ </AuthKitProvider>,
437
+ );
438
+
439
+ await act(async () => {
440
+ getByRole('button').click();
441
+ });
442
+
443
+ expect(handleSignOutAction).toHaveBeenCalled();
444
+ });
445
+
446
+ it('should pass returnTo parameter to handleSignOutAction', async () => {
447
+ (handleSignOutAction as jest.Mock).mockResolvedValueOnce({});
448
+
449
+ const TestComponent = () => {
450
+ const auth = useAuth();
451
+ return (
452
+ <div>
453
+ <div data-testid="session">{auth.sessionId}</div>
454
+ <button onClick={() => auth.signOut({ returnTo: '/home' })}>Sign out</button>
455
+ </div>
456
+ );
457
+ };
458
+
459
+ const { getByRole } = render(
460
+ <AuthKitProvider>
461
+ <TestComponent />
462
+ </AuthKitProvider>,
463
+ );
464
+
465
+ await act(async () => {
466
+ getByRole('button').click();
467
+ });
468
+
469
+ expect(handleSignOutAction).toHaveBeenCalledWith({ returnTo: '/home' });
470
+ });
471
+ });
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { Button } from './button.js';
5
+
6
+ describe('Button', () => {
7
+ it('should render with default props', () => {
8
+ const { getByRole } = render(<Button>Click me</Button>);
9
+ const button = getByRole('button');
10
+
11
+ expect(button).toBeInTheDocument();
12
+ expect(button).toHaveTextContent('Click me');
13
+ expect(button).toHaveAttribute('type', 'button');
14
+ });
15
+
16
+ it('should forward ref correctly', () => {
17
+ const ref = React.createRef<HTMLButtonElement>();
18
+ render(<Button ref={ref}>Click me</Button>);
19
+
20
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement);
21
+ });
22
+
23
+ it('should merge custom styles with default styles', () => {
24
+ const { getByRole } = render(<Button style={{ backgroundColor: 'red' }}>Click me</Button>);
25
+ const button = getByRole('button');
26
+
27
+ expect(button).toHaveStyle({
28
+ backgroundColor: 'red',
29
+ display: 'inline-flex',
30
+ alignItems: 'center',
31
+ justifyContent: 'center',
32
+ });
33
+ });
34
+
35
+ it('should pass through additional props', () => {
36
+ const { getByRole } = render(
37
+ <Button data-testid="test-button" aria-label="Test Button">
38
+ Click me
39
+ </Button>,
40
+ );
41
+ const button = getByRole('button');
42
+
43
+ expect(button).toHaveAttribute('data-testid', 'test-button');
44
+ expect(button).toHaveAttribute('aria-label', 'Test Button');
45
+ });
46
+ });
@@ -0,0 +1,134 @@
1
+ import { render, act, screen } from '@testing-library/react';
2
+ import '@testing-library/jest-dom';
3
+ import { Impersonation } from './impersonation.js';
4
+ import { useAuth } from './authkit-provider.js';
5
+ import { getOrganizationAction } from '../actions.js';
6
+ import * as React from 'react';
7
+ import { handleSignOutAction } from '../actions.js';
8
+
9
+ // Mock the useAuth hook
10
+ jest.mock('./authkit-provider', () => ({
11
+ useAuth: jest.fn(),
12
+ }));
13
+
14
+ // Mock the getOrganizationAction
15
+ jest.mock('../actions', () => ({
16
+ getOrganizationAction: jest.fn(),
17
+ handleSignOutAction: jest.fn(),
18
+ }));
19
+
20
+ describe('Impersonation', () => {
21
+ beforeEach(() => {
22
+ jest.clearAllMocks();
23
+ });
24
+
25
+ it('should return null if not impersonating', () => {
26
+ (useAuth as jest.Mock).mockReturnValue({
27
+ impersonator: null,
28
+ user: { id: '123', email: 'user@example.com' },
29
+ organizationId: null,
30
+ loading: false,
31
+ });
32
+
33
+ const { container } = render(<Impersonation />);
34
+ expect(container).toBeEmptyDOMElement();
35
+ });
36
+
37
+ it('should return null if loading', () => {
38
+ (useAuth as jest.Mock).mockReturnValue({
39
+ impersonator: { email: 'admin@example.com' },
40
+ user: { id: '123', email: 'user@example.com' },
41
+ organizationId: null,
42
+ loading: true,
43
+ });
44
+
45
+ const { container } = render(<Impersonation />);
46
+ expect(container).toBeEmptyDOMElement();
47
+ });
48
+
49
+ it('should render impersonation banner when impersonating', () => {
50
+ (useAuth as jest.Mock).mockReturnValue({
51
+ impersonator: { email: 'admin@example.com' },
52
+ user: { id: '123', email: 'user@example.com' },
53
+ organizationId: null,
54
+ loading: false,
55
+ });
56
+
57
+ const { container } = render(<Impersonation />);
58
+ expect(container.querySelector('[data-workos-impersonation-root]')).toBeInTheDocument();
59
+ });
60
+
61
+ it('should render with organization info when organizationId is provided', async () => {
62
+ (useAuth as jest.Mock).mockReturnValue({
63
+ impersonator: { email: 'admin@example.com' },
64
+ user: { id: '123', email: 'user@example.com' },
65
+ organizationId: 'org_123',
66
+ loading: false,
67
+ });
68
+
69
+ (getOrganizationAction as jest.Mock).mockResolvedValue({
70
+ id: 'org_123',
71
+ name: 'Test Org',
72
+ });
73
+
74
+ const { container } = await act(async () => {
75
+ return render(<Impersonation />);
76
+ });
77
+
78
+ expect(container.querySelector('[data-workos-impersonation-root]')).toBeInTheDocument();
79
+ });
80
+
81
+ it('should render at the bottom by default', () => {
82
+ (useAuth as jest.Mock).mockReturnValue({
83
+ impersonator: { email: 'admin@example.com' },
84
+ user: { id: '123', email: 'user@example.com' },
85
+ organizationId: null,
86
+ loading: false,
87
+ });
88
+
89
+ const { container } = render(<Impersonation />);
90
+ const banner = container.querySelector('[data-workos-impersonation-root] > div:nth-child(2)');
91
+ expect(banner).toHaveStyle({ bottom: 'var(--wi-s)' });
92
+ });
93
+
94
+ it('should render at the top when side prop is "top"', () => {
95
+ (useAuth as jest.Mock).mockReturnValue({
96
+ impersonator: { email: 'admin@example.com' },
97
+ user: { id: '123', email: 'user@example.com' },
98
+ organizationId: null,
99
+ loading: false,
100
+ });
101
+
102
+ const { container } = render(<Impersonation side="top" />);
103
+ const banner = container.querySelector('[data-workos-impersonation-root] > div:nth-child(2)');
104
+ expect(banner).toHaveStyle({ top: 'var(--wi-s)' });
105
+ });
106
+
107
+ it('should merge custom styles with default styles', () => {
108
+ (useAuth as jest.Mock).mockReturnValue({
109
+ impersonator: { email: 'admin@example.com' },
110
+ user: { id: '123', email: 'user@example.com' },
111
+ organizationId: null,
112
+ loading: false,
113
+ });
114
+
115
+ const customStyle = { backgroundColor: 'red' };
116
+ const { container } = render(<Impersonation style={customStyle} />);
117
+ const root = container.querySelector('[data-workos-impersonation-root]');
118
+ expect(root).toHaveStyle({ backgroundColor: 'red' });
119
+ });
120
+
121
+ it('should should sign out when the Stop button is called', async () => {
122
+ (useAuth as jest.Mock).mockReturnValue({
123
+ impersonator: { email: 'admin@example.com' },
124
+ user: { id: '123', email: 'user@example.com' },
125
+ organizationId: null,
126
+ loading: false,
127
+ });
128
+
129
+ render(<Impersonation />);
130
+ const stopButton = await screen.findByText('Stop');
131
+ stopButton.click();
132
+ expect(handleSignOutAction).toHaveBeenCalled();
133
+ });
134
+ });
@@ -0,0 +1,60 @@
1
+ import { render, act } from '@testing-library/react';
2
+ import { MinMaxButton } from './min-max-button.js';
3
+ import * as React from 'react';
4
+ import '@testing-library/jest-dom';
5
+
6
+ describe('MinMaxButton', () => {
7
+ beforeEach(() => {
8
+ // Create the root element before each test
9
+ const root = document.createElement('div');
10
+ root.setAttribute('data-workos-impersonation-root', '');
11
+ document.body.appendChild(root);
12
+ });
13
+
14
+ afterEach(() => {
15
+ // Clean up after each test
16
+ document.body.innerHTML = '';
17
+ });
18
+
19
+ it('sets minimized value when clicked', () => {
20
+ const { getByRole } = render(<MinMaxButton minimizedValue="1">Minimize</MinMaxButton>);
21
+
22
+ act(() => {
23
+ getByRole('button').click();
24
+ });
25
+
26
+ const root = document.querySelector('[data-workos-impersonation-root]');
27
+ expect(root).toHaveStyle({ '--wi-minimized': '1' });
28
+ });
29
+
30
+ it('does nothing if root is undefined', () => {
31
+ const { getByRole } = render(<MinMaxButton minimizedValue="1">Minimize</MinMaxButton>);
32
+
33
+ const root = document.querySelector('[data-workos-impersonation-root]');
34
+
35
+ // Mock querySelector to return null for this test
36
+ jest.spyOn(document, 'querySelector').mockReturnValue(null);
37
+
38
+ act(() => {
39
+ getByRole('button').click();
40
+ });
41
+
42
+ expect(root).not.toHaveStyle({ '--wi-minimized': '1' });
43
+ });
44
+
45
+ it('renders children correctly', () => {
46
+ const { getByText } = render(<MinMaxButton minimizedValue="0">Test Child</MinMaxButton>);
47
+
48
+ expect(getByText('Test Child')).toBeInTheDocument();
49
+ });
50
+
51
+ it('applies correct default styling', () => {
52
+ const { getByRole } = render(<MinMaxButton minimizedValue="0">Test</MinMaxButton>);
53
+
54
+ const button = getByRole('button');
55
+ expect(button).toHaveStyle({
56
+ padding: 0,
57
+ width: '1.714em',
58
+ });
59
+ });
60
+ });