@workos-inc/authkit-nextjs 2.6.0 → 2.7.1
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.
- package/README.md +124 -29
- package/dist/esm/components/tokenStore.js +110 -11
- package/dist/esm/components/tokenStore.js.map +1 -1
- package/dist/esm/components/useAccessToken.js +6 -1
- package/dist/esm/components/useAccessToken.js.map +1 -1
- package/dist/esm/cookie.js +51 -0
- package/dist/esm/cookie.js.map +1 -1
- package/dist/esm/middleware.js +2 -2
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/session.js +35 -2
- package/dist/esm/session.js.map +1 -1
- package/dist/esm/test-helpers.js +57 -0
- package/dist/esm/test-helpers.js.map +1 -0
- package/dist/esm/types/components/tokenStore.d.ts +7 -2
- package/dist/esm/types/cookie.d.ts +1 -0
- package/dist/esm/types/interfaces.d.ts +2 -0
- package/dist/esm/types/middleware.d.ts +1 -1
- package/dist/esm/types/session.d.ts +1 -1
- package/dist/esm/types/test-helpers.d.ts +3 -0
- package/dist/esm/types/workos.d.ts +1 -1
- package/dist/esm/workos.js +1 -1
- package/package.json +4 -3
- package/src/actions.spec.ts +100 -0
- package/src/auth.spec.ts +347 -0
- package/src/authkit-callback-route.spec.ts +258 -0
- package/src/components/authkit-provider.spec.tsx +471 -0
- package/src/components/button.spec.tsx +46 -0
- package/src/components/impersonation.spec.tsx +134 -0
- package/src/components/min-max-button.spec.tsx +60 -0
- package/src/components/tokenStore.spec.ts +816 -0
- package/src/components/tokenStore.ts +147 -12
- package/src/components/useAccessToken.spec.tsx +731 -0
- package/src/components/useAccessToken.ts +6 -1
- package/src/components/useTokenClaims.spec.tsx +194 -0
- package/src/cookie.spec.ts +276 -0
- package/src/cookie.ts +56 -0
- package/src/get-authorization-url.spec.ts +60 -0
- package/src/interfaces.ts +2 -0
- package/src/jwt.spec.ts +159 -0
- package/src/middleware.ts +2 -1
- package/src/session.spec.ts +1162 -0
- package/src/session.ts +41 -1
- package/src/test-helpers.ts +70 -0
- package/src/utils.spec.ts +142 -0
- package/src/workos.spec.ts +67 -0
- 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
|
+
});
|