Skip to content

Commit e8e3f5f

Browse files
committed
refactor: using a modal instead of confirm(), added tests
1 parent f95963f commit e8e3f5f

File tree

8 files changed

+400
-89
lines changed

8 files changed

+400
-89
lines changed

phpmyfaq/admin/assets/src/api/faqs.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,13 @@
1414
*/
1515

1616
import { fetchJson } from './fetch-wrapper';
17-
import { Response } from '../interfaces';
18-
import { FaqList } from '../interfaces';
1917

2018
export const fetchAllFaqsByCategory = async (
2119
categoryId: string,
2220
language: string,
2321
onlyInactive?: boolean,
2422
onlyNew?: boolean
25-
): Promise<FaqList> => {
23+
): Promise<unknown> => {
2624
let currentUrl: string = window.location.protocol + '//' + window.location.host;
2725
let pathname: string = window.location.pathname;
2826

@@ -49,7 +47,7 @@ export const fetchAllFaqsByCategory = async (
4947
});
5048
};
5149

52-
export const fetchFaqsByAutocomplete = async (searchTerm: string, csrfToken: string): Promise<Response | undefined> => {
50+
export const fetchFaqsByAutocomplete = async (searchTerm: string, csrfToken: string): Promise<unknown> => {
5351
return await fetchJson(`./api/faq/search`, {
5452
method: 'POST',
5553
headers: {
@@ -63,7 +61,7 @@ export const fetchFaqsByAutocomplete = async (searchTerm: string, csrfToken: str
6361
});
6462
};
6563

66-
export const deleteFaq = async (faqId: string, faqLanguage: string, token: string): Promise<Response | undefined> => {
64+
export const deleteFaq = async (faqId: string, faqLanguage: string, token: string): Promise<unknown> => {
6765
return await fetchJson('./api/faq/delete', {
6866
method: 'DELETE',
6967
headers: {
@@ -78,7 +76,7 @@ export const deleteFaq = async (faqId: string, faqLanguage: string, token: strin
7876
});
7977
};
8078

81-
export const create = async (formData: unknown): Promise<Response | undefined> => {
79+
export const create = async (formData: unknown): Promise<unknown> => {
8280
return await fetchJson('./api/faq/create', {
8381
method: 'POST',
8482
headers: {
@@ -91,7 +89,7 @@ export const create = async (formData: unknown): Promise<Response | undefined> =
9189
});
9290
};
9391

94-
export const update = async (formData: unknown): Promise<Response | undefined> => {
92+
export const update = async (formData: unknown): Promise<unknown> => {
9593
return await fetchJson('./api/faq/update', {
9694
method: 'PUT',
9795
headers: {
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { updateStickyFaqsOrder, removeStickyFaq } from './sticky-faqs';
3+
import * as fetchWrapperModule from './fetch-wrapper';
4+
5+
// Mock the fetch-wrapper module
6+
vi.mock('./fetch-wrapper', () => ({
7+
fetchJson: vi.fn(),
8+
}));
9+
10+
describe('sticky-faqs API', () => {
11+
beforeEach(() => {
12+
vi.clearAllMocks();
13+
});
14+
15+
afterEach(() => {
16+
vi.restoreAllMocks();
17+
});
18+
19+
describe('updateStickyFaqsOrder', () => {
20+
it('should update sticky FAQ order successfully', async () => {
21+
const mockResponse = {
22+
success: 'Order updated successfully',
23+
};
24+
25+
vi.spyOn(fetchWrapperModule, 'fetchJson').mockResolvedValue(mockResponse);
26+
27+
const faqIds = ['1', '2', '3'];
28+
const csrf = 'test-csrf-token';
29+
30+
const result = await updateStickyFaqsOrder(faqIds, csrf);
31+
32+
expect(fetchWrapperModule.fetchJson).toHaveBeenCalledWith('./api/faqs/sticky/order', {
33+
method: 'POST',
34+
headers: {
35+
Accept: 'application/json, text/plain, */*',
36+
'Content-Type': 'application/json',
37+
},
38+
body: JSON.stringify({
39+
faqIds,
40+
csrf,
41+
}),
42+
});
43+
44+
expect(result).toEqual(mockResponse);
45+
});
46+
47+
it('should handle empty FAQ IDs array', async () => {
48+
const mockResponse = {
49+
success: 'Order updated',
50+
};
51+
52+
vi.spyOn(fetchWrapperModule, 'fetchJson').mockResolvedValue(mockResponse);
53+
54+
const result = await updateStickyFaqsOrder([], 'csrf-token');
55+
56+
expect(fetchWrapperModule.fetchJson).toHaveBeenCalledWith(
57+
'./api/faqs/sticky/order',
58+
expect.objectContaining({
59+
method: 'POST',
60+
body: JSON.stringify({
61+
faqIds: [],
62+
csrf: 'csrf-token',
63+
}),
64+
})
65+
);
66+
67+
expect(result).toEqual(mockResponse);
68+
});
69+
70+
it('should handle API error response', async () => {
71+
const mockError = new Error('API Error');
72+
73+
vi.spyOn(fetchWrapperModule, 'fetchJson').mockRejectedValue(mockError);
74+
75+
await expect(updateStickyFaqsOrder(['1'], 'csrf-token')).rejects.toThrow('API Error');
76+
});
77+
78+
it('should send correct request body format', async () => {
79+
vi.spyOn(fetchWrapperModule, 'fetchJson').mockResolvedValue({ success: 'OK' });
80+
81+
await updateStickyFaqsOrder(['10', '20', '30'], 'my-csrf');
82+
83+
const callArgs = vi.mocked(fetchWrapperModule.fetchJson).mock.calls[0];
84+
const requestBody = JSON.parse(callArgs[1]?.body as string);
85+
86+
expect(requestBody).toEqual({
87+
faqIds: ['10', '20', '30'],
88+
csrf: 'my-csrf',
89+
});
90+
});
91+
});
92+
93+
describe('removeStickyFaq', () => {
94+
it('should remove sticky FAQ successfully', async () => {
95+
const mockResponse = {
96+
success: 'FAQ removed from sticky list',
97+
};
98+
99+
vi.spyOn(fetchWrapperModule, 'fetchJson').mockResolvedValue(mockResponse);
100+
101+
const faqId = '42';
102+
const categoryId = '5';
103+
const csrfToken = 'test-csrf-token';
104+
const lang = 'en';
105+
106+
const result = await removeStickyFaq(faqId, categoryId, csrfToken, lang);
107+
108+
expect(fetchWrapperModule.fetchJson).toHaveBeenCalledWith('./api/faq/sticky', {
109+
method: 'POST',
110+
headers: {
111+
Accept: 'application/json',
112+
'Content-Type': 'application/json',
113+
'X-PMF-CSRF-Token': csrfToken,
114+
'X-Requested-With': 'XMLHttpRequest',
115+
},
116+
body: JSON.stringify({
117+
csrf: csrfToken,
118+
categoryId: categoryId,
119+
faqIds: [faqId],
120+
faqLanguage: lang,
121+
checked: false,
122+
}),
123+
});
124+
125+
expect(result).toEqual(mockResponse);
126+
});
127+
128+
it('should include correct headers', async () => {
129+
vi.spyOn(fetchWrapperModule, 'fetchJson').mockResolvedValue({ success: 'OK' });
130+
131+
await removeStickyFaq('1', '2', 'my-token', 'de');
132+
133+
const callArgs = vi.mocked(fetchWrapperModule.fetchJson).mock.calls[0];
134+
const headers = callArgs[1]?.headers as Record<string, string>;
135+
136+
expect(headers['X-PMF-CSRF-Token']).toBe('my-token');
137+
expect(headers['X-Requested-With']).toBe('XMLHttpRequest');
138+
expect(headers['Content-Type']).toBe('application/json');
139+
expect(headers['Accept']).toBe('application/json');
140+
});
141+
142+
it('should send faqId as array in request body', async () => {
143+
vi.spyOn(fetchWrapperModule, 'fetchJson').mockResolvedValue({ success: 'OK' });
144+
145+
await removeStickyFaq('123', '456', 'csrf', 'fr');
146+
147+
const callArgs = vi.mocked(fetchWrapperModule.fetchJson).mock.calls[0];
148+
const requestBody = JSON.parse(callArgs[1]?.body as string);
149+
150+
expect(requestBody.faqIds).toEqual(['123']);
151+
expect(Array.isArray(requestBody.faqIds)).toBe(true);
152+
});
153+
154+
it('should set checked to false', async () => {
155+
vi.spyOn(fetchWrapperModule, 'fetchJson').mockResolvedValue({ success: 'OK' });
156+
157+
await removeStickyFaq('1', '2', 'csrf', 'en');
158+
159+
const callArgs = vi.mocked(fetchWrapperModule.fetchJson).mock.calls[0];
160+
const requestBody = JSON.parse(callArgs[1]?.body as string);
161+
162+
expect(requestBody.checked).toBe(false);
163+
});
164+
165+
it('should handle API error response', async () => {
166+
const mockError = new Error('Network error');
167+
168+
vi.spyOn(fetchWrapperModule, 'fetchJson').mockRejectedValue(mockError);
169+
170+
await expect(removeStickyFaq('1', '2', 'csrf', 'en')).rejects.toThrow('Network error');
171+
});
172+
173+
it('should handle different language codes', async () => {
174+
vi.spyOn(fetchWrapperModule, 'fetchJson').mockResolvedValue({ success: 'OK' });
175+
176+
await removeStickyFaq('1', '2', 'csrf', 'de');
177+
178+
const callArgs = vi.mocked(fetchWrapperModule.fetchJson).mock.calls[0];
179+
const requestBody = JSON.parse(callArgs[1]?.body as string);
180+
181+
expect(requestBody.faqLanguage).toBe('de');
182+
});
183+
184+
it('should send all required parameters in request body', async () => {
185+
vi.spyOn(fetchWrapperModule, 'fetchJson').mockResolvedValue({ success: 'OK' });
186+
187+
const faqId = '999';
188+
const categoryId = '888';
189+
const csrf = 'token-123';
190+
const lang = 'es';
191+
192+
await removeStickyFaq(faqId, categoryId, csrf, lang);
193+
194+
const callArgs = vi.mocked(fetchWrapperModule.fetchJson).mock.calls[0];
195+
const requestBody = JSON.parse(callArgs[1]?.body as string);
196+
197+
expect(requestBody).toEqual({
198+
csrf: csrf,
199+
categoryId: categoryId,
200+
faqIds: [faqId],
201+
faqLanguage: lang,
202+
checked: false,
203+
});
204+
});
205+
});
206+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* API functions for sticky FAQs management
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public License,
5+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
6+
* obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* @package phpMyFAQ
9+
* @author Thorsten Rinne <[email protected]>
10+
* @copyright 2026 phpMyFAQ Team
11+
* @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
12+
* @link https://www.phpmyfaq.de
13+
* @since 2026-01-18
14+
*/
15+
16+
import { fetchJson } from './fetch-wrapper';
17+
18+
export interface StickyOrderResponse {
19+
success?: string;
20+
error?: string;
21+
}
22+
23+
export interface RemoveStickyResponse {
24+
success?: string;
25+
error?: string;
26+
}
27+
28+
/**
29+
* Update the order of sticky FAQs
30+
* @param faqIds Array of FAQ IDs in the new order
31+
* @param csrf CSRF token
32+
* @returns Promise with the API response
33+
*/
34+
export const updateStickyFaqsOrder = async (faqIds: string[], csrf: string): Promise<StickyOrderResponse> => {
35+
return (await fetchJson('./api/faqs/sticky/order', {
36+
method: 'POST',
37+
headers: {
38+
Accept: 'application/json, text/plain, */*',
39+
'Content-Type': 'application/json',
40+
},
41+
body: JSON.stringify({
42+
faqIds,
43+
csrf,
44+
}),
45+
})) as StickyOrderResponse;
46+
};
47+
48+
/**
49+
* Remove a sticky FAQ
50+
* @param faqId FAQ ID to remove from sticky
51+
* @param categoryId Category ID
52+
* @param csrfToken CSRF token
53+
* @param lang Language code
54+
* @returns Promise with the API response
55+
*/
56+
export const removeStickyFaq = async (
57+
faqId: string,
58+
categoryId: string,
59+
csrfToken: string,
60+
lang: string
61+
): Promise<RemoveStickyResponse> => {
62+
return (await fetchJson('./api/faq/sticky', {
63+
method: 'POST',
64+
headers: {
65+
Accept: 'application/json',
66+
'Content-Type': 'application/json',
67+
'X-PMF-CSRF-Token': csrfToken,
68+
'X-Requested-With': 'XMLHttpRequest',
69+
},
70+
body: JSON.stringify({
71+
csrf: csrfToken,
72+
categoryId: categoryId,
73+
faqIds: [faqId],
74+
faqLanguage: lang,
75+
checked: false,
76+
}),
77+
})) as RemoveStickyResponse;
78+
};

0 commit comments

Comments
 (0)