@micha.bigler/ui-core-micha 2.10.1 → 2.12.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.
@@ -0,0 +1,138 @@
1
+ // @vitest-environment jsdom
2
+ import React from 'react';
3
+ import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+
6
+ const authApi = vi.hoisted(() => ({
7
+ submitRegistrationRequest: vi.fn(),
8
+ }));
9
+
10
+ vi.mock('../src/auth/authApi', () => authApi);
11
+ vi.mock('react-router-dom', () => ({
12
+ useNavigate: () => vi.fn(),
13
+ useLocation: () => ({ search: '' }),
14
+ }));
15
+ vi.mock('react-helmet', () => ({ Helmet: ({ children }) => <>{children}</> }));
16
+ vi.mock('react-i18next', () => ({
17
+ useTranslation: () => ({ t: (key, fallback) => (typeof fallback === 'string' ? fallback : key) }),
18
+ }));
19
+
20
+ import { AuthContext } from '../src/auth/AuthContext';
21
+ import { SignUpPage } from '../src/pages/SignUpPage';
22
+
23
+ function renderSignUp({ signupModes, siteKey }) {
24
+ return render(
25
+ <AuthContext.Provider value={{ authMethods: {
26
+ signup: true,
27
+ signup_modes: signupModes,
28
+ turnstile_site_key: siteKey,
29
+ } }}>
30
+ <SignUpPage />
31
+ </AuthContext.Provider>,
32
+ );
33
+ }
34
+
35
+ describe('SignUpPage Turnstile integration', () => {
36
+ beforeEach(() => {
37
+ vi.clearAllMocks();
38
+ authApi.submitRegistrationRequest.mockResolvedValue({});
39
+ window.turnstile = {
40
+ render: vi.fn(() => 'widget-id'),
41
+ remove: vi.fn(),
42
+ reset: vi.fn(),
43
+ };
44
+ });
45
+
46
+ afterEach(() => {
47
+ cleanup();
48
+ delete window.turnstile;
49
+ });
50
+
51
+ it('renders only for open and email-domain signup modes when the site key is present', async () => {
52
+ renderSignUp({
53
+ signupModes: ['self_signup_open', 'self_signup_access_code', 'self_signup_email_domain'],
54
+ siteKey: 'site-key',
55
+ });
56
+
57
+ await waitFor(() => expect(screen.getByTestId('turnstile-widget')).toBeTruthy());
58
+ fireEvent.click(screen.getByRole('button', { name: 'self_signup_access_code' }));
59
+ expect(screen.queryByTestId('turnstile-widget')).toBeNull();
60
+
61
+ fireEvent.click(screen.getByRole('button', { name: 'self_signup_email_domain' }));
62
+ await waitFor(() => expect(screen.getByTestId('turnstile-widget')).toBeTruthy());
63
+ });
64
+
65
+ it('gates submission until verified and passes the Turnstile token to the registration API', async () => {
66
+ renderSignUp({ signupModes: ['self_signup_open'], siteKey: 'site-key' });
67
+
68
+ await waitFor(() => expect(window.turnstile.render).toHaveBeenCalledOnce());
69
+ fireEvent.change(screen.getByRole('textbox', { name: /Auth\.EMAIL_LABEL/ }), {
70
+ target: { value: 'ada@example.com' },
71
+ });
72
+
73
+ const submit = screen.getByRole('button', { name: 'Auth.SIGNUP_SUBMIT' });
74
+ expect(submit.disabled).toBe(true);
75
+
76
+ await act(async () => {
77
+ window.turnstile.render.mock.calls[0][1].callback('turnstile-token');
78
+ });
79
+
80
+ await waitFor(() => expect(submit.disabled).toBe(false));
81
+ fireEvent.click(submit);
82
+
83
+ await waitFor(() => expect(authApi.submitRegistrationRequest).toHaveBeenCalledWith(
84
+ expect.objectContaining({
85
+ email: 'ada@example.com',
86
+ mode: 'self_signup_open',
87
+ turnstileToken: 'turnstile-token',
88
+ }),
89
+ ));
90
+ });
91
+
92
+ it('resets the widget after a failed protected signup submission', async () => {
93
+ authApi.submitRegistrationRequest.mockRejectedValue({ code: 'Auth.INVITE_FAILED' });
94
+ renderSignUp({ signupModes: ['self_signup_open'], siteKey: 'site-key' });
95
+
96
+ await waitFor(() => expect(window.turnstile.render).toHaveBeenCalledOnce());
97
+ fireEvent.change(screen.getByRole('textbox', { name: /Auth\.EMAIL_LABEL/ }), {
98
+ target: { value: 'ada@example.com' },
99
+ });
100
+ await act(async () => {
101
+ window.turnstile.render.mock.calls[0][1].callback('turnstile-token');
102
+ });
103
+
104
+ fireEvent.click(screen.getByRole('button', { name: 'Auth.SIGNUP_SUBMIT' }));
105
+
106
+ await waitFor(() => expect(window.turnstile.reset).toHaveBeenCalledWith('widget-id'));
107
+ expect(screen.getByRole('button', { name: 'Auth.SIGNUP_SUBMIT' }).disabled).toBe(true);
108
+ });
109
+
110
+ it('resets the widget and gates submission when its token expires', async () => {
111
+ renderSignUp({ signupModes: ['self_signup_open'], siteKey: 'site-key' });
112
+
113
+ await waitFor(() => expect(window.turnstile.render).toHaveBeenCalledOnce());
114
+ await act(async () => {
115
+ window.turnstile.render.mock.calls[0][1].callback('turnstile-token');
116
+ });
117
+ await waitFor(() => expect(screen.getByRole('button', { name: 'Auth.SIGNUP_SUBMIT' }).disabled).toBe(false));
118
+
119
+ await act(async () => {
120
+ window.turnstile.render.mock.calls[0][1]['expired-callback']();
121
+ });
122
+
123
+ expect(window.turnstile.reset).toHaveBeenCalledWith('widget-id');
124
+ expect(screen.getByRole('button', { name: 'Auth.SIGNUP_SUBMIT' }).disabled).toBe(true);
125
+ });
126
+
127
+ it.each([
128
+ ['self_signup_access_code', 'site-key'],
129
+ ['self_signup_qr', 'site-key'],
130
+ ['self_signup_open', ''],
131
+ ])('does not render for %s without an eligible key/mode combination', async (mode, siteKey) => {
132
+ renderSignUp({ signupModes: [mode], siteKey });
133
+
134
+ await waitFor(() => expect(screen.getByRole('button', { name: 'Auth.SIGNUP_SUBMIT' })).toBeTruthy());
135
+ expect(screen.queryByTestId('turnstile-widget')).toBeNull();
136
+ expect(window.turnstile.render).not.toHaveBeenCalled();
137
+ });
138
+ });
@@ -0,0 +1,31 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const client = vi.hoisted(() => ({
4
+ post: vi.fn(),
5
+ }));
6
+
7
+ vi.mock('../src/auth/apiClient', () => ({ default: client }));
8
+
9
+ import { submitRegistrationRequest } from '../src/auth/authApi';
10
+
11
+ describe('submitRegistrationRequest', () => {
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+
16
+ it('includes a Turnstile token in the registration payload', async () => {
17
+ client.post.mockResolvedValue({ data: { ok: true } });
18
+
19
+ await submitRegistrationRequest({
20
+ email: 'ada@example.com',
21
+ mode: 'self_signup_open',
22
+ turnstileToken: 'turnstile-token',
23
+ });
24
+
25
+ expect(client.post).toHaveBeenCalledWith('/api/users/register-request/', {
26
+ email: 'ada@example.com',
27
+ mode: 'self_signup_open',
28
+ turnstile_token: 'turnstile-token',
29
+ });
30
+ });
31
+ });
@@ -0,0 +1,46 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const client = vi.hoisted(() => ({
4
+ get: vi.fn(),
5
+ post: vi.fn(),
6
+ }));
7
+
8
+ vi.mock('../src/auth/apiClient', () => ({ default: client }));
9
+
10
+ import { getNotificationFeed, getUnreadCount, markNotifications } from '../src/notifications/feedApi';
11
+
12
+ describe('notification feed API', () => {
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ });
16
+
17
+ it('loads the feed with an optional status filter', async () => {
18
+ const data = { count: 1, results: [] };
19
+ client.get.mockResolvedValue({ data });
20
+
21
+ await expect(getNotificationFeed({ status: 'unseen' })).resolves.toBe(data);
22
+
23
+ expect(client.get).toHaveBeenCalledWith('/api/notifications/feed/', { params: { status: 'unseen' } });
24
+ });
25
+
26
+ it('loads the unread count', async () => {
27
+ const data = { count: 3 };
28
+ client.get.mockResolvedValue({ data });
29
+
30
+ await expect(getUnreadCount()).resolves.toBe(data);
31
+
32
+ expect(client.get).toHaveBeenCalledWith('/api/notifications/feed/unread-count/');
33
+ });
34
+
35
+ it('marks recipient rows with the supplied action and ids', async () => {
36
+ const data = { updated: 2 };
37
+ client.post.mockResolvedValue({ data });
38
+
39
+ await expect(markNotifications({ action: 'seen', ids: [4, 8] })).resolves.toBe(data);
40
+
41
+ expect(client.post).toHaveBeenCalledWith('/api/notifications/feed/mark/', {
42
+ action: 'seen',
43
+ ids: [4, 8],
44
+ });
45
+ });
46
+ });
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ NotificationSettings,
5
+ getNotificationPreferences,
6
+ getVapidPublicKey,
7
+ listPushSubscriptions,
8
+ notificationsTranslations,
9
+ patchNotificationPreferences,
10
+ removePushSubscription,
11
+ savePushSubscription,
12
+ urlBase64ToUint8Array,
13
+ } from '../src/index';
14
+
15
+ describe('notification barrel exports', () => {
16
+ it('preserves legacy notification exports and translations', () => {
17
+ [
18
+ NotificationSettings,
19
+ getNotificationPreferences,
20
+ patchNotificationPreferences,
21
+ getVapidPublicKey,
22
+ listPushSubscriptions,
23
+ savePushSubscription,
24
+ removePushSubscription,
25
+ urlBase64ToUint8Array,
26
+ notificationsTranslations,
27
+ ].forEach((value) => expect(value).toBeDefined());
28
+ });
29
+ });