@c-rex/contexts 0.3.0-build.40 → 0.3.0-build.42
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c-rex/contexts",
|
|
3
|
-
"version": "0.3.0-build.
|
|
3
|
+
"version": "0.3.0-build.42",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src"
|
|
@@ -24,16 +24,24 @@
|
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"lint": "eslint .",
|
|
27
|
-
"lint:fix": "eslint . --fix"
|
|
27
|
+
"lint:fix": "eslint . --fix",
|
|
28
|
+
"test": "jest",
|
|
29
|
+
"test:watch": "jest --watch"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
30
32
|
"@c-rex/eslint-config": "*",
|
|
31
33
|
"@c-rex/typescript-config": "*",
|
|
32
34
|
"@turbo/gen": "^2.4.4",
|
|
35
|
+
"@types/jest": "^29.5.14",
|
|
33
36
|
"@types/node": "^22.13.10",
|
|
34
37
|
"@types/react": "19.0.10",
|
|
35
38
|
"@types/react-dom": "19.0.4",
|
|
39
|
+
"@testing-library/react": "^16.3.0",
|
|
40
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
36
41
|
"eslint": "^9.23.0",
|
|
42
|
+
"jest": "^29.7.0",
|
|
43
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
44
|
+
"ts-jest": "^29.3.2",
|
|
37
45
|
"typescript": "latest"
|
|
38
46
|
},
|
|
39
47
|
"dependencies": {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { renderHook, act } from '@testing-library/react';
|
|
4
|
+
import { AppConfigProvider, useAppConfig } from '../config-provider';
|
|
5
|
+
import type { CookiesConfigs, LanguageAndCountries } from '@c-rex/interfaces';
|
|
6
|
+
|
|
7
|
+
const mockSetAvailableLanguages = jest.fn();
|
|
8
|
+
const mockUpdatePreferences = jest.fn();
|
|
9
|
+
const mockSetCookie = jest.fn();
|
|
10
|
+
const mockCall = jest.fn();
|
|
11
|
+
|
|
12
|
+
jest.mock('@c-rex/components/language-store', () => ({
|
|
13
|
+
useLanguageStore: {
|
|
14
|
+
getState: () => ({ setAvailableLanguages: mockSetAvailableLanguages }),
|
|
15
|
+
},
|
|
16
|
+
}), { virtual: true });
|
|
17
|
+
|
|
18
|
+
jest.mock('@c-rex/components/search-settings-store', () => ({
|
|
19
|
+
useSearchSettingsStore: {
|
|
20
|
+
getState: () => ({ updatePreferences: mockUpdatePreferences }),
|
|
21
|
+
},
|
|
22
|
+
}), { virtual: true });
|
|
23
|
+
|
|
24
|
+
jest.mock('@c-rex/utils/cookies', () => ({
|
|
25
|
+
setCookie: (...args: unknown[]) => mockSetCookie(...args),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
jest.mock('@c-rex/utils', () => ({
|
|
29
|
+
call: (...args: unknown[]) => mockCall(...args),
|
|
30
|
+
normalizeLanguageCode: (lang: string) => lang,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const mockFetch = jest.fn();
|
|
34
|
+
global.fetch = mockFetch;
|
|
35
|
+
|
|
36
|
+
const initialConfig: CookiesConfigs = {
|
|
37
|
+
projectName: 'test-project',
|
|
38
|
+
titles: { main: 'Main', sub: 'Sub' },
|
|
39
|
+
languageSwitcher: { enabled: true, default: 'en', endpoint: '/api/lang' },
|
|
40
|
+
publicNextApiUrl: 'http://localhost:3001',
|
|
41
|
+
OIDC: { clientEnabled: false, userEnabled: false },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const availableLanguages: LanguageAndCountries[] = [
|
|
45
|
+
{ country: 'US', lang: 'en', value: 'en-US' },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const makeWrapper = (config: CookiesConfigs = initialConfig) =>
|
|
49
|
+
({ children }: { children: React.ReactNode }) => (
|
|
50
|
+
<AppConfigProvider
|
|
51
|
+
uiLang="en-us"
|
|
52
|
+
contentLang="en"
|
|
53
|
+
availableLanguages={availableLanguages}
|
|
54
|
+
initialConfig={config}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</AppConfigProvider>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Flush all pending async state updates in the component
|
|
61
|
+
const flushEffects = async () => act(async () => { await Promise.resolve(); });
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
jest.clearAllMocks();
|
|
65
|
+
mockFetch.mockResolvedValue({
|
|
66
|
+
ok: true,
|
|
67
|
+
json: async () => ({ value: 'token-value' }),
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('AppConfigProvider', () => {
|
|
72
|
+
it('renders children and provides initialConfig', async () => {
|
|
73
|
+
const { result } = renderHook(() => useAppConfig(), { wrapper: makeWrapper() });
|
|
74
|
+
await flushEffects();
|
|
75
|
+
expect(result.current.configs).toEqual(initialConfig);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('packageID is null initially', async () => {
|
|
79
|
+
const { result } = renderHook(() => useAppConfig(), { wrapper: makeWrapper() });
|
|
80
|
+
await flushEffects();
|
|
81
|
+
expect(result.current.packageID).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('articleLang is null initially', async () => {
|
|
85
|
+
const { result } = renderHook(() => useAppConfig(), { wrapper: makeWrapper() });
|
|
86
|
+
await flushEffects();
|
|
87
|
+
expect(result.current.articleLang).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('availableVersions is null initially', async () => {
|
|
91
|
+
const { result } = renderHook(() => useAppConfig(), { wrapper: makeWrapper() });
|
|
92
|
+
await flushEffects();
|
|
93
|
+
expect(result.current.availableVersions).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('setPackageID updates packageID', async () => {
|
|
97
|
+
const { result } = renderHook(() => useAppConfig(), { wrapper: makeWrapper() });
|
|
98
|
+
await flushEffects();
|
|
99
|
+
act(() => { result.current.setPackageID('pkg-123'); });
|
|
100
|
+
expect(result.current.packageID).toBe('pkg-123');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('setArticleLang updates articleLang', async () => {
|
|
104
|
+
const { result } = renderHook(() => useAppConfig(), { wrapper: makeWrapper() });
|
|
105
|
+
await flushEffects();
|
|
106
|
+
act(() => { result.current.setArticleLang('de'); });
|
|
107
|
+
expect(result.current.articleLang).toBe('de');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('setAvailableVersions updates availableVersions', async () => {
|
|
111
|
+
const { result } = renderHook(() => useAppConfig(), { wrapper: makeWrapper() });
|
|
112
|
+
await flushEffects();
|
|
113
|
+
const versions = [{ id: 'v1', label: 'v1.0', lang: 'en', packageId: 'pkg-1' }];
|
|
114
|
+
act(() => { result.current.setAvailableVersions(versions as any); });
|
|
115
|
+
expect(result.current.availableVersions).toEqual(versions);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('setCookie is called with UI_LANG_KEY on mount', async () => {
|
|
119
|
+
renderHook(() => useAppConfig(), { wrapper: makeWrapper() });
|
|
120
|
+
await flushEffects();
|
|
121
|
+
expect(mockSetCookie).toHaveBeenCalledWith('UI_LANG_KEY', 'en-us', { httpOnly: false });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('setAvailableLanguages is called with provided languages on mount', async () => {
|
|
125
|
+
renderHook(() => useAppConfig(), { wrapper: makeWrapper() });
|
|
126
|
+
await flushEffects();
|
|
127
|
+
expect(mockSetAvailableLanguages).toHaveBeenCalledWith(availableLanguages);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('updatePreferences is called with normalized content lang on mount', async () => {
|
|
131
|
+
renderHook(() => useAppConfig(), { wrapper: makeWrapper() });
|
|
132
|
+
await flushEffects();
|
|
133
|
+
expect(mockUpdatePreferences).toHaveBeenCalledWith({ language: 'en' });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('throws when used outside AppConfigProvider', () => {
|
|
137
|
+
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
138
|
+
expect(() => {
|
|
139
|
+
renderHook(() => useAppConfig());
|
|
140
|
+
}).toThrow('useAppConfig must be used within AppConfigProvider');
|
|
141
|
+
spy.mockRestore();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { renderHook, act } from '@testing-library/react';
|
|
4
|
+
import { HighlightProvider, useHighlight } from '../highlight-provider';
|
|
5
|
+
|
|
6
|
+
Element.prototype.scrollIntoView = jest.fn();
|
|
7
|
+
|
|
8
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
9
|
+
<HighlightProvider>{children}</HighlightProvider>
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
describe('HighlightProvider', () => {
|
|
13
|
+
it('starts with currentIndex=-1 and total=0', () => {
|
|
14
|
+
const { result } = renderHook(() => useHighlight(), { wrapper });
|
|
15
|
+
expect(result.current.currentIndex).toBe(-1);
|
|
16
|
+
expect(result.current.total).toBe(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('registerContainer with marks updates total and sets currentIndex=0', () => {
|
|
20
|
+
const { result } = renderHook(() => useHighlight(), { wrapper });
|
|
21
|
+
const container = document.createElement('div');
|
|
22
|
+
container.innerHTML = '<mark>A</mark><mark>B</mark>';
|
|
23
|
+
act(() => { result.current.registerContainer(container); });
|
|
24
|
+
expect(result.current.total).toBe(2);
|
|
25
|
+
expect(result.current.currentIndex).toBe(0);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('registerContainer with no marks keeps total=0 and currentIndex=-1', () => {
|
|
29
|
+
const { result } = renderHook(() => useHighlight(), { wrapper });
|
|
30
|
+
const container = document.createElement('div');
|
|
31
|
+
act(() => { result.current.registerContainer(container); });
|
|
32
|
+
expect(result.current.total).toBe(0);
|
|
33
|
+
expect(result.current.currentIndex).toBe(-1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('registerContainer(null) resets state', () => {
|
|
37
|
+
const { result } = renderHook(() => useHighlight(), { wrapper });
|
|
38
|
+
const container = document.createElement('div');
|
|
39
|
+
container.innerHTML = '<mark>A</mark>';
|
|
40
|
+
act(() => { result.current.registerContainer(container); });
|
|
41
|
+
act(() => { result.current.registerContainer(null); });
|
|
42
|
+
expect(result.current.total).toBe(0);
|
|
43
|
+
expect(result.current.currentIndex).toBe(-1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('next() advances currentIndex', () => {
|
|
47
|
+
const { result } = renderHook(() => useHighlight(), { wrapper });
|
|
48
|
+
const container = document.createElement('div');
|
|
49
|
+
container.innerHTML = '<mark>A</mark><mark>B</mark><mark>C</mark>';
|
|
50
|
+
act(() => { result.current.registerContainer(container); });
|
|
51
|
+
act(() => { result.current.next(); });
|
|
52
|
+
expect(result.current.currentIndex).toBe(1);
|
|
53
|
+
act(() => { result.current.next(); });
|
|
54
|
+
expect(result.current.currentIndex).toBe(2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('next() wraps around to 0 after last mark', () => {
|
|
58
|
+
const { result } = renderHook(() => useHighlight(), { wrapper });
|
|
59
|
+
const container = document.createElement('div');
|
|
60
|
+
container.innerHTML = '<mark>A</mark><mark>B</mark>';
|
|
61
|
+
act(() => { result.current.registerContainer(container); });
|
|
62
|
+
act(() => { result.current.next(); });
|
|
63
|
+
act(() => { result.current.next(); });
|
|
64
|
+
expect(result.current.currentIndex).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('prev() wraps to last mark from index 0', () => {
|
|
68
|
+
const { result } = renderHook(() => useHighlight(), { wrapper });
|
|
69
|
+
const container = document.createElement('div');
|
|
70
|
+
container.innerHTML = '<mark>A</mark><mark>B</mark><mark>C</mark>';
|
|
71
|
+
act(() => { result.current.registerContainer(container); });
|
|
72
|
+
act(() => { result.current.prev(); });
|
|
73
|
+
expect(result.current.currentIndex).toBe(2);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('refreshMarks re-syncs marks from container', () => {
|
|
77
|
+
const { result } = renderHook(() => useHighlight(), { wrapper });
|
|
78
|
+
const container = document.createElement('div');
|
|
79
|
+
container.innerHTML = '<mark>A</mark>';
|
|
80
|
+
act(() => { result.current.registerContainer(container); });
|
|
81
|
+
expect(result.current.total).toBe(1);
|
|
82
|
+
container.innerHTML = '<mark>A</mark><mark>B</mark><mark>C</mark>';
|
|
83
|
+
act(() => { result.current.refreshMarks(); });
|
|
84
|
+
expect(result.current.total).toBe(3);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('throws when used outside HighlightProvider', () => {
|
|
88
|
+
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
89
|
+
expect(() => {
|
|
90
|
+
renderHook(() => useHighlight());
|
|
91
|
+
}).toThrow('useHighlight must be used inside HighlightProvider');
|
|
92
|
+
spy.mockRestore();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { renderHook, act } from '@testing-library/react';
|
|
4
|
+
import { SearchProvider, useSearchContext } from '../search';
|
|
5
|
+
|
|
6
|
+
jest.mock('@c-rex/components/loading', () => ({
|
|
7
|
+
Loading: () => <div data-testid="loading-overlay" />,
|
|
8
|
+
}), { virtual: true });
|
|
9
|
+
|
|
10
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
11
|
+
<SearchProvider>{children}</SearchProvider>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
describe('SearchContext', () => {
|
|
15
|
+
it('provides loading=false initially', () => {
|
|
16
|
+
const { result } = renderHook(() => useSearchContext(), { wrapper });
|
|
17
|
+
expect(result.current.loading).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('exposes setLoading function', () => {
|
|
21
|
+
const { result } = renderHook(() => useSearchContext(), { wrapper });
|
|
22
|
+
expect(typeof result.current.setLoading).toBe('function');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('setLoading(true) updates loading state', () => {
|
|
26
|
+
const { result } = renderHook(() => useSearchContext(), { wrapper });
|
|
27
|
+
act(() => { result.current.setLoading(true); });
|
|
28
|
+
expect(result.current.loading).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('setLoading(false) resets loading state', () => {
|
|
32
|
+
const { result } = renderHook(() => useSearchContext(), { wrapper });
|
|
33
|
+
act(() => { result.current.setLoading(true); });
|
|
34
|
+
act(() => { result.current.setLoading(false); });
|
|
35
|
+
expect(result.current.loading).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('throws when used outside SearchProvider', () => {
|
|
39
|
+
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
40
|
+
expect(() => {
|
|
41
|
+
renderHook(() => useSearchContext());
|
|
42
|
+
}).toThrow('useSearchContext must be used within a SearchProvider');
|
|
43
|
+
spy.mockRestore();
|
|
44
|
+
});
|
|
45
|
+
});
|