@websolutespa/bom-next 3.0.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 (130) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +11 -0
  3. package/dist/config.client-B8As5DY0.js +2 -0
  4. package/dist/config.edge-B8As5DY0.js +2 -0
  5. package/dist/config.server-B8As5DY0.js +2 -0
  6. package/dist/exports/client-xcFJNcG7.d.ts +979 -0
  7. package/dist/exports/client-xcFJNcG7.d.ts.map +1 -0
  8. package/dist/exports/client.js +5735 -0
  9. package/dist/exports/client.js.map +1 -0
  10. package/dist/exports/instrumentation-DBO6QH4d.d.ts +13 -0
  11. package/dist/exports/instrumentation-DBO6QH4d.d.ts.map +1 -0
  12. package/dist/exports/instrumentation.js +25 -0
  13. package/dist/exports/instrumentation.js.map +1 -0
  14. package/dist/exports/proxy-Cv7QFVci.d.ts +71 -0
  15. package/dist/exports/proxy-Cv7QFVci.d.ts.map +1 -0
  16. package/dist/exports/proxy.js +254 -0
  17. package/dist/exports/proxy.js.map +1 -0
  18. package/dist/exports/server-BHObJHJ6.d.ts +402 -0
  19. package/dist/exports/server-BHObJHJ6.d.ts.map +1 -0
  20. package/dist/exports/server.js +1974 -0
  21. package/dist/exports/server.js.map +1 -0
  22. package/dist/http-_2M-UFRx.js +61 -0
  23. package/dist/http-_2M-UFRx.js.map +1 -0
  24. package/dist/index-CdYcvudT.d.ts +4 -0
  25. package/dist/index-CtfAJ0y7.d.ts +499 -0
  26. package/dist/index-CtfAJ0y7.d.ts.map +1 -0
  27. package/dist/index.js +201 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/route-DzFwERQP.js +65 -0
  30. package/dist/route-DzFwERQP.js.map +1 -0
  31. package/dist/store-CJzqrHsl.js +73 -0
  32. package/dist/store-CJzqrHsl.js.map +1 -0
  33. package/dist/types-BatUTdbZ.d.ts +15 -0
  34. package/dist/types-BatUTdbZ.d.ts.map +1 -0
  35. package/dist/types-D29_snYY.d.ts +191 -0
  36. package/dist/types-D29_snYY.d.ts.map +1 -0
  37. package/dist/utils-Ca7xh5mm.js +78 -0
  38. package/dist/utils-Ca7xh5mm.js.map +1 -0
  39. package/dist/utils-YwteTbk7.js +23 -0
  40. package/dist/utils-YwteTbk7.js.map +1 -0
  41. package/package.json +156 -0
  42. package/src/client/client.tsx +18 -0
  43. package/src/client/extra.tsx +21 -0
  44. package/src/client/label.tsx +24 -0
  45. package/src/client/layout.tsx +22 -0
  46. package/src/client/local.collection.ts +33 -0
  47. package/src/client/metadata.tsx +217 -0
  48. package/src/client/page.tsx +22 -0
  49. package/src/client/useApi.test.tsx +203 -0
  50. package/src/client/useApi.ts +59 -0
  51. package/src/client/useCollectionApi.test.tsx +201 -0
  52. package/src/client/useCollectionApi.ts +66 -0
  53. package/src/client/useExtra.tsx +12 -0
  54. package/src/client/useHttp.ts +54 -0
  55. package/src/client/useLabel.test.tsx +76 -0
  56. package/src/client/useLabel.tsx +12 -0
  57. package/src/client/useLayout.tsx +13 -0
  58. package/src/client/useLink.tsx +13 -0
  59. package/src/client/useMetaData.tsx +12 -0
  60. package/src/client/usePage.tsx +12 -0
  61. package/src/client/useUser.tsx +12 -0
  62. package/src/client/user.tsx +43 -0
  63. package/src/exports/client.ts +20 -0
  64. package/src/exports/instrumentation.ts +1 -0
  65. package/src/exports/proxy.ts +5 -0
  66. package/src/exports/server.ts +19 -0
  67. package/src/global.d.ts +6 -0
  68. package/src/index.ts +17 -0
  69. package/src/instrumentation/client.ts +19 -0
  70. package/src/instrumentation/sentry/capture.requestError.ts +6 -0
  71. package/src/instrumentation/sentry/capture.routerTransitionStart.ts +4 -0
  72. package/src/instrumentation/sentry/config.client.ts +9 -0
  73. package/src/instrumentation/sentry/config.edge.ts +9 -0
  74. package/src/instrumentation/sentry/config.server.ts +9 -0
  75. package/src/instrumentation/server.ts +24 -0
  76. package/src/instrumentation/types.ts +12 -0
  77. package/src/lib/breadcrumb.ts +21 -0
  78. package/src/lib/const.ts +3 -0
  79. package/src/lib/http.test.ts +25 -0
  80. package/src/lib/http.ts +74 -0
  81. package/src/lib/label.ts +43 -0
  82. package/src/lib/layout.defaults.ts +167 -0
  83. package/src/lib/link.ts +19 -0
  84. package/src/lib/meta-data.ts +60 -0
  85. package/src/lib/open-graph.ts +165 -0
  86. package/src/lib/pagination.ts +78 -0
  87. package/src/lib/payload.config.ts +12 -0
  88. package/src/lib/route.ts +83 -0
  89. package/src/lib/store.ts +63 -0
  90. package/src/lib/structured-data.ts +24 -0
  91. package/src/lib/twitter-card.ts +49 -0
  92. package/src/lib/utils.ts +45 -0
  93. package/src/mock.ts +264 -0
  94. package/src/proxy/protected-route.test.ts +75 -0
  95. package/src/proxy/protected-route.ts +57 -0
  96. package/src/proxy/proxy.test.ts +48 -0
  97. package/src/proxy/proxy.ts +64 -0
  98. package/src/proxy/route-auto-detection.ts +126 -0
  99. package/src/proxy/route-proxy.ts +187 -0
  100. package/src/server/context.ts +147 -0
  101. package/src/server/handlers/auth.handler.ts +27 -0
  102. package/src/server/handlers/captions.handler.ts +32 -0
  103. package/src/server/handlers/collection.handler.test.ts +57 -0
  104. package/src/server/handlers/collection.handler.ts +212 -0
  105. package/src/server/handlers/error-response.ts +45 -0
  106. package/src/server/handlers/index.handler.ts +146 -0
  107. package/src/server/handlers/preview.handler.ts +63 -0
  108. package/src/server/handlers/registry.handler.test.ts +65 -0
  109. package/src/server/handlers/registry.handler.ts +259 -0
  110. package/src/server/handlers/route.handler.ts +161 -0
  111. package/src/server/handlers/sitemap.handler.ts +374 -0
  112. package/src/server/handlers/static-params.handler.ts +111 -0
  113. package/src/server/handlers/vars.handler.ts +167 -0
  114. package/src/server/layout.ts +120 -0
  115. package/src/server/lazy.ts +41 -0
  116. package/src/server/local-api.ts +63 -0
  117. package/src/server/metadata.ts +87 -0
  118. package/src/server/mixer-payload.ts +124 -0
  119. package/src/server/mixer-payload.type-test.ts +27 -0
  120. package/src/server/mixer.ts +439 -0
  121. package/src/server/mock/category.ts +34 -0
  122. package/src/server/mock/collection.ts +21 -0
  123. package/src/server/mock/page.ts +62 -0
  124. package/src/server/mock/route.ts +36 -0
  125. package/src/server/mock/store.ts +159 -0
  126. package/src/server/page.ts +86 -0
  127. package/src/server/session/session.ts +101 -0
  128. package/src/server/utils.ts +23 -0
  129. package/src/server/utmz.ts +11 -0
  130. package/src/types.ts +211 -0
@@ -0,0 +1,203 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import '@testing-library/jest-dom/vitest';
4
+ import { cleanup, render, renderHook, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
5
+ import { ILayout } from '@websolutespa/bom-core';
6
+ import { afterAll, afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
7
+ import { getLocalApiUrl } from '../lib/http';
8
+ import { LayoutDefaults } from '../lib/layout.defaults';
9
+ import { LabelProvider } from './label';
10
+ import { LayoutProvider } from './layout';
11
+ import { useApi, useApiDelete, useApiFetch, useApiGet, useApiPatch, useApiPost, useApiPut } from './useApi';
12
+
13
+ const wrapper = (props: { children: React.ReactNode }) => (
14
+ <LayoutProvider layout={LayoutDefaults as ILayout}>
15
+ <LabelProvider>
16
+ {props.children}
17
+ </LabelProvider>
18
+ </LayoutProvider>
19
+ );
20
+
21
+ const defaultResponse = {
22
+ ok: true,
23
+ status: 200,
24
+ json: () => Promise.resolve(null),
25
+ };
26
+
27
+ function mockFetch(
28
+ method: string = 'GET',
29
+ result: any = { message: 'ok' },
30
+ overrideResponse: Partial<typeof defaultResponse> = {}
31
+ ) {
32
+ const endpoint = '/endpoint';
33
+ const url = `${getLocalApiUrl(endpoint)}?locale=en&market=ww`;
34
+ const payload = { data: 'data' };
35
+ const response = {
36
+ ...defaultResponse,
37
+ ...overrideResponse,
38
+ json: () => Promise.resolve(result),
39
+ } as Response;
40
+ (global.fetch as any) = vi.fn(() =>
41
+ Promise.resolve(response)
42
+ );
43
+ const request: {
44
+ body?: string;
45
+ headers: { [key: string]: string },
46
+ method: string;
47
+ } = {
48
+ headers: {
49
+ 'Content-Type': 'application/json',
50
+ },
51
+ method,
52
+ };
53
+ if (['POST', 'PATCH', 'PUT'].includes(method)) {
54
+ request.body = JSON.stringify(payload);
55
+ }
56
+ return { endpoint, url, payload, result, request, response };
57
+ }
58
+
59
+ describe('useApi', () => {
60
+ const ENV = process.env;
61
+ const FETCH = global.fetch;
62
+
63
+ afterEach(() => {
64
+ cleanup();
65
+ });
66
+
67
+ beforeEach(() => {
68
+ vi.resetModules();
69
+ vi.resetAllMocks();
70
+ vi.spyOn(console, 'error').mockImplementation(() => { });
71
+ vi.spyOn(console, 'log').mockImplementation(() => { });
72
+ process.env = {
73
+ MIXER_SECRET: 'MIXER_SECRET_32_CHARS_LONG',
74
+ NEXT_PUBLIC_API: '/api',
75
+ NEXT_PUBLIC_MOCK_FALLBACK_URL: 'http://localhost:3000',
76
+ NEXT_PUBLIC_URL: 'http://localhost:3000',
77
+ NODE_ENV: 'test',
78
+ STORE_API_KEY: 'STORE_API_KEY',
79
+ STORE_API: '/bowl/api',
80
+ STORE_STRATEGY: 'mock',
81
+ STORE_URL: 'http://localhost:4000',
82
+ };
83
+ });
84
+
85
+ afterAll(() => {
86
+ process.env = ENV;
87
+ global.fetch = FETCH;
88
+ });
89
+
90
+ test('should throw without provider', () => {
91
+ expect(() => {
92
+ renderHook(() => useApi());
93
+ }).toThrow('useLayout must be used within LayoutProvider');
94
+ });
95
+
96
+ test('should not throw with provider', () => {
97
+ expect(() => {
98
+ renderHook(() => useApi(), { wrapper });
99
+ }).not.toThrow();
100
+ });
101
+
102
+ test('api.get should return ok', async () => {
103
+ const { result } = renderHook(() => useApi(), { wrapper });
104
+ const mock = mockFetch();
105
+ const response = await result.current.get(mock.endpoint);
106
+ expect(fetch).toHaveBeenCalledTimes(1);
107
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
108
+ expect(response).toBe(mock.result);
109
+ });
110
+
111
+ test('api.post should return ok', async () => {
112
+ const { result } = renderHook(() => useApi(), { wrapper });
113
+ const mock = mockFetch('POST');
114
+ const response = await result.current.post(mock.endpoint, mock.payload);
115
+ expect(fetch).toHaveBeenCalledTimes(1);
116
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
117
+ expect(response).toBe(mock.result);
118
+ });
119
+
120
+ test('api.patch should return ok', async () => {
121
+ const { result } = renderHook(() => useApi(), { wrapper });
122
+ const mock = mockFetch('PATCH');
123
+ const response = await result.current.patch(mock.endpoint, mock.payload);
124
+ expect(fetch).toHaveBeenCalledTimes(1);
125
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
126
+ expect(response).toBe(mock.result);
127
+ });
128
+
129
+ test('api.put should return ok', async () => {
130
+ const { result } = renderHook(() => useApi(), { wrapper });
131
+ const mock = mockFetch('PUT');
132
+ const response = await result.current.put(mock.endpoint, mock.payload);
133
+ expect(fetch).toHaveBeenCalledTimes(1);
134
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
135
+ expect(response).toBe(mock.result);
136
+ });
137
+
138
+ test('api.delete should return ok', async () => {
139
+ const { result } = renderHook(() => useApi(), { wrapper });
140
+ const mock = mockFetch('DELETE');
141
+ const response = await result.current.delete(mock.endpoint);
142
+ expect(fetch).toHaveBeenCalledTimes(1);
143
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
144
+ expect(response).toBe(mock.result);
145
+ });
146
+
147
+ test('useApiGet expect result ok', TestMethod('GET'));
148
+
149
+ test('useApiPost expect result ok', TestMethod('POST'));
150
+
151
+ test('useApiPatch expect result ok', TestMethod('PATCH'));
152
+
153
+ test('useApiPut expect result ok', TestMethod('PUT'));
154
+
155
+ test('useApiDelete expect result ok', TestMethod('DELETE'));
156
+
157
+ function TestMethod(method?: string) {
158
+ const useHook = (mock: ReturnType<typeof mockFetch>) => {
159
+ switch (method) {
160
+ case 'GET':
161
+ return useApiGet(mock.endpoint);
162
+ case 'POST':
163
+ return useApiPost(mock.endpoint, mock.payload);
164
+ case 'PATCH':
165
+ return useApiPatch(mock.endpoint, mock.payload);
166
+ case 'PUT':
167
+ return useApiPut(mock.endpoint, mock.payload);
168
+ case 'DELETE':
169
+ return useApiDelete(mock.endpoint);
170
+ default:
171
+ return useApiFetch(mock.endpoint);
172
+ }
173
+ };
174
+ return async () => {
175
+ const mock = mockFetch(method);
176
+ const Wrapper = () => {
177
+ const [response, loading, error, setError] = useHook(mock);
178
+ return (
179
+ <>
180
+ {loading && <div data-testid="loading">loading</div>}
181
+ {response && <div data-testid="result">{JSON.stringify(response)}</div>}
182
+ {error && <div data-testid="error">{JSON.stringify(error)}</div>}
183
+ </>
184
+ );
185
+ };
186
+ render(
187
+ <LayoutProvider layout={LayoutDefaults as ILayout}>
188
+ <LabelProvider>
189
+ <Wrapper />
190
+ </LabelProvider>
191
+ </LayoutProvider>
192
+ );
193
+ expect(screen.getByTestId('loading')).toHaveTextContent('loading');
194
+ await waitForElementToBeRemoved(() => screen.getByTestId('loading'));
195
+ expect(fetch).toHaveBeenCalledTimes(1);
196
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
197
+ const result = await waitFor(() => screen.getByTestId('result'));
198
+ expect(result).toHaveTextContent(JSON.stringify(mock.result));
199
+ };
200
+ }
201
+
202
+ });
203
+
@@ -0,0 +1,59 @@
1
+ import { ApiRequestOptions, localApiDelete, localApiFetch, localApiGet, localApiPatch, localApiPost, localApiPut } from '../lib/http';
2
+ import { useHttpFetch } from './useHttp';
3
+ import { useLayout } from './useLayout';
4
+
5
+ export function useApiFetch<T = unknown>(pathname: string, options: ApiRequestOptions = {}) {
6
+ const layout = useLayout();
7
+ // const layout = { locale: '', market: '' };
8
+ return useHttpFetch<T>(pathname, options, localApiFetch, layout);
9
+ }
10
+
11
+ export function useApiGet<T = unknown>(pathname: string, options: ApiRequestOptions = {}) {
12
+ return useApiFetch<T>(pathname, { ...options, method: 'GET' });
13
+ }
14
+
15
+ export function useApiPost<T = unknown>(pathname: string, payload: unknown, options: ApiRequestOptions = {}) {
16
+ return useApiFetch<T>(pathname, { ...options, method: 'POST', body: payload ? JSON.stringify(payload) : undefined });
17
+ }
18
+
19
+ export function useApiPut<T = unknown>(pathname: string, payload: unknown, options: ApiRequestOptions = {}) {
20
+ return useApiFetch<T>(pathname, { ...options, method: 'PUT', body: payload ? JSON.stringify(payload) : undefined });
21
+ }
22
+
23
+ export function useApiPatch<T = unknown>(pathname: string, payload: unknown, options: ApiRequestOptions = {}) {
24
+ return useApiFetch<T>(pathname, { ...options, method: 'PATCH', body: payload ? JSON.stringify(payload) : undefined });
25
+ }
26
+
27
+ export function useApiDelete<T = unknown>(pathname: string, options: ApiRequestOptions = {}) {
28
+ return useApiFetch<T>(pathname, { ...options, method: 'DELETE' });
29
+ }
30
+
31
+ type Api = {
32
+ get: <T = unknown>(pathname: string, options?: ApiRequestOptions) => Promise<T>;
33
+ post: <T = unknown>(pathname: string, payload?: unknown, options?: ApiRequestOptions) => Promise<T>;
34
+ put: <T = unknown>(pathname: string, payload?: unknown, options?: ApiRequestOptions) => Promise<T>;
35
+ patch: <T = unknown>(pathname: string, payload?: unknown, options?: ApiRequestOptions) => Promise<T>;
36
+ delete: <T = unknown>(pathname: string, options?: ApiRequestOptions) => Promise<T>;
37
+ };
38
+
39
+ export function useApi(): Api {
40
+ const layout = useLayout();
41
+ // const layout = { locale: '', market: '' };
42
+ return {
43
+ get: async <T = unknown>(pathname: string, options: ApiRequestOptions = {}) => {
44
+ return localApiGet<T>(pathname, options, layout);
45
+ },
46
+ post: async <T = unknown>(pathname: string, payload?: unknown, options: ApiRequestOptions = {}) => {
47
+ return localApiPost<T>(pathname, payload, options, layout);
48
+ },
49
+ put: async <T = unknown>(pathname: string, payload?: unknown, options: ApiRequestOptions = {}) => {
50
+ return localApiPut<T>(pathname, payload, options, layout);
51
+ },
52
+ patch: async <T = unknown>(pathname: string, payload?: unknown, options: ApiRequestOptions = {}) => {
53
+ return localApiPatch<T>(pathname, payload, options, layout);
54
+ },
55
+ delete: async <T = unknown>(pathname: string, options: ApiRequestOptions = {}) => {
56
+ return localApiDelete<T>(pathname, options, layout);
57
+ },
58
+ };
59
+ }
@@ -0,0 +1,201 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import '@testing-library/jest-dom/vitest';
4
+ import { cleanup, render, renderHook, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
5
+ import { ILayout } from '@websolutespa/bom-core';
6
+ import { afterAll, afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
7
+ import { getLocalApiUrl } from '../lib/http';
8
+ import { LayoutDefaults } from '../lib/layout.defaults';
9
+ import { LayoutProvider } from './layout';
10
+ import { useApiFetch } from './useApi';
11
+ import { useCollectionApi, useCollectionApiDelete, useCollectionApiGet, useCollectionApiPatch, useCollectionApiPost, useCollectionApiPut } from './useCollectionApi';
12
+
13
+ const wrapper = (props: { children: React.ReactNode }) => (
14
+ <LayoutProvider layout={LayoutDefaults as ILayout}>{props.children}</LayoutProvider>
15
+ );
16
+
17
+ const defaultResponse = {
18
+ ok: true,
19
+ status: 200,
20
+ json: () => Promise.resolve(null),
21
+ };
22
+
23
+ function mockFetch(
24
+ method: string = 'GET',
25
+ result: any = { message: 'ok' },
26
+ overrideResponse: Partial<typeof defaultResponse> = {}
27
+ ) {
28
+ const endpoint = '/endpoint';
29
+ const url = `${getLocalApiUrl(endpoint)}?locale=en&market=ww`;
30
+ const payload = { data: 'data' };
31
+ const response = {
32
+ ...defaultResponse,
33
+ ...overrideResponse,
34
+ json: () => Promise.resolve(result),
35
+ } as Response;
36
+ (global.fetch as any) = vi.fn(() =>
37
+ Promise.resolve(response)
38
+ );
39
+ const request: {
40
+ body?: string;
41
+ headers: { [key: string]: string },
42
+ method: string;
43
+ } = {
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ },
47
+ method,
48
+ };
49
+ if (['POST', 'PATCH', 'PUT'].includes(method)) {
50
+ request.body = JSON.stringify(payload);
51
+ }
52
+ return { endpoint, url, payload, result, request, response };
53
+ }
54
+
55
+ describe('useCollectionApi', () => {
56
+ const ENV = process.env;
57
+ const FETCH = global.fetch;
58
+
59
+ afterEach(() => {
60
+ cleanup();
61
+ });
62
+
63
+ beforeEach(() => {
64
+ vi.resetModules();
65
+ vi.resetAllMocks();
66
+ vi.spyOn(console, 'error').mockImplementation(() => { });
67
+ vi.spyOn(console, 'log').mockImplementation(() => { });
68
+ process.env = {
69
+ MIXER_SECRET: 'MIXER_SECRET_32_CHARS_LONG',
70
+ NEXT_PUBLIC_API: '/api',
71
+ NEXT_PUBLIC_MOCK_FALLBACK_URL: 'http://localhost:3000',
72
+ NEXT_PUBLIC_URL: 'http://localhost:3000',
73
+ NODE_ENV: 'test',
74
+ STORE_API_KEY: 'STORE_API_KEY',
75
+ STORE_API: '/bowl/api',
76
+ STORE_STRATEGY: 'mock',
77
+ STORE_URL: 'http://localhost:4000',
78
+ };
79
+ });
80
+
81
+ afterAll(() => {
82
+ process.env = ENV;
83
+ global.fetch = FETCH;
84
+ });
85
+
86
+ test('should throw without provider', () => {
87
+ expect(() => {
88
+ renderHook(() => useCollectionApi());
89
+ }).toThrow('useLayout must be used within LayoutProvider');
90
+ });
91
+
92
+ test('should not throw with provider', () => {
93
+ expect(() => {
94
+ renderHook(() => useCollectionApi(), { wrapper });
95
+ }).not.toThrow();
96
+ });
97
+
98
+ test('api.get should return ok', async () => {
99
+ const { result } = renderHook(() => useCollectionApi(), { wrapper });
100
+ const mock = mockFetch();
101
+ const response = await result.current.get(mock.endpoint);
102
+ expect(fetch).toHaveBeenCalledTimes(1);
103
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
104
+ expect(response).toBe(mock.result);
105
+ });
106
+
107
+ test('api.post should return ok', async () => {
108
+ const { result } = renderHook(() => useCollectionApi(), { wrapper });
109
+ const mock = mockFetch('POST');
110
+ const response = await result.current.post(mock.endpoint, mock.payload);
111
+ expect(fetch).toHaveBeenCalledTimes(1);
112
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
113
+ expect(response).toBe(mock.result);
114
+ });
115
+
116
+ test('api.patch should return ok', async () => {
117
+ const { result } = renderHook(() => useCollectionApi(), { wrapper });
118
+ const mock = mockFetch('PATCH');
119
+ const response = await result.current.patch(mock.endpoint, mock.payload);
120
+ expect(fetch).toHaveBeenCalledTimes(1);
121
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
122
+ expect(response).toBe(mock.result);
123
+ });
124
+
125
+ test('api.put should return ok', async () => {
126
+ const { result } = renderHook(() => useCollectionApi(), { wrapper });
127
+ const mock = mockFetch('PUT');
128
+ const response = await result.current.put(mock.endpoint, mock.payload);
129
+ expect(fetch).toHaveBeenCalledTimes(1);
130
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
131
+ expect(response).toBe(mock.result);
132
+ });
133
+
134
+ test('api.delete should return ok', async () => {
135
+ const { result } = renderHook(() => useCollectionApi(), { wrapper });
136
+ const mock = mockFetch('DELETE');
137
+ const response = await result.current.delete(mock.endpoint);
138
+ expect(fetch).toHaveBeenCalledTimes(1);
139
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
140
+ expect(response).toBe(mock.result);
141
+ });
142
+
143
+ test('useCollectionApiGet expect result ok', TestMethod('GET'));
144
+
145
+ test('useCollectionApiPost expect result ok', TestMethod('POST'));
146
+
147
+ test('useCollectionApiPatch expect result ok', TestMethod('PATCH'));
148
+
149
+ test('useCollectionApiPut expect result ok', TestMethod('PUT'));
150
+
151
+ test('useCollectionApiDelete expect result ok', TestMethod('DELETE'));
152
+
153
+ function TestMethod(method?: string) {
154
+ const useHook = (mock: ReturnType<typeof mockFetch>) => {
155
+ switch (method) {
156
+ case 'GET':
157
+ // eslint-disable-next-line react-hooks/rules-of-hooks
158
+ return useCollectionApiGet(mock.endpoint);
159
+ case 'POST':
160
+ // eslint-disable-next-line react-hooks/rules-of-hooks
161
+ return useCollectionApiPost(mock.endpoint, mock.payload);
162
+ case 'PATCH':
163
+ // eslint-disable-next-line react-hooks/rules-of-hooks
164
+ return useCollectionApiPatch(mock.endpoint, mock.payload);
165
+ case 'PUT':
166
+ // eslint-disable-next-line react-hooks/rules-of-hooks
167
+ return useCollectionApiPut(mock.endpoint, mock.payload);
168
+ case 'DELETE':
169
+ // eslint-disable-next-line react-hooks/rules-of-hooks
170
+ return useCollectionApiDelete(mock.endpoint);
171
+ default:
172
+ // eslint-disable-next-line react-hooks/rules-of-hooks
173
+ return useApiFetch(mock.endpoint);
174
+ }
175
+ };
176
+ return async () => {
177
+ const mock = mockFetch(method);
178
+ const Wrapper = () => {
179
+ const [response, loading, error, setError] = useHook(mock);
180
+ return (
181
+ <>
182
+ {loading && <div data-testid="loading">loading</div>}
183
+ {response && <div data-testid="result">{JSON.stringify(response)}</div>}
184
+ {error && <div data-testid="error">{JSON.stringify(error)}</div>}
185
+ </>
186
+ );
187
+ };
188
+ render(
189
+ <LayoutProvider layout={LayoutDefaults as ILayout}><Wrapper /></LayoutProvider>
190
+ );
191
+ expect(screen.getByTestId('loading')).toHaveTextContent('loading');
192
+ await waitForElementToBeRemoved(() => screen.getByTestId('loading'));
193
+ expect(fetch).toHaveBeenCalledTimes(1);
194
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
195
+ const result = await waitFor(() => screen.getByTestId('result'));
196
+ expect(result).toHaveTextContent(JSON.stringify(mock.result));
197
+ };
198
+ }
199
+
200
+ });
201
+
@@ -0,0 +1,66 @@
1
+ import { QueryParams, qsSerialize } from '@websolutespa/bom-core';
2
+ import { ApiRequestOptions } from '../lib/http';
3
+ import { localCollectionDelete, localCollectionGet, localCollectionPatch, localCollectionPost, localCollectionPut } from './local.collection';
4
+ import { useApiFetch } from './useApi';
5
+ import { useLayout } from './useLayout';
6
+
7
+ type CollectionApi = {
8
+ get: <T = unknown>(pathname: string, params?: QueryParams, options?: ApiRequestOptions) => Promise<T>;
9
+ post: <T = unknown>(pathname: string, payload?: unknown, options?: ApiRequestOptions) => Promise<T>;
10
+ put: <T = unknown>(pathname: string, payload?: unknown, options?: ApiRequestOptions) => Promise<T>;
11
+ patch: <T = unknown>(pathname: string, payload?: unknown, options?: ApiRequestOptions) => Promise<T>;
12
+ delete: <T = unknown>(pathname: string, options?: ApiRequestOptions) => Promise<T>;
13
+ };
14
+
15
+ export function useCollectionApi(): CollectionApi {
16
+ const layout = useLayout();
17
+ return {
18
+ get: async <T = unknown>(pathname: string, params: QueryParams = {}, options: ApiRequestOptions = {}) => {
19
+ params.locale = params.locale || layout.locale;
20
+ params.market = params.market || layout.market;
21
+ const qs = qsSerialize(params);
22
+ return localCollectionGet<T>(pathname + (qs ? '?' + qs : ''), options);
23
+ },
24
+ post: async <T = unknown>(pathname: string, payload?: unknown, options: ApiRequestOptions = {}) => {
25
+ return localCollectionPost<T>(pathname, payload, options, layout);
26
+ },
27
+ put: async <T = unknown>(pathname: string, payload?: unknown, options: ApiRequestOptions = {}) => {
28
+ return localCollectionPut<T>(pathname, payload, options, layout);
29
+ },
30
+ patch: async <T = unknown>(pathname: string, payload?: unknown, options: ApiRequestOptions = {}) => {
31
+ return localCollectionPatch<T>(pathname, payload, options, layout);
32
+ },
33
+ delete: async <T = unknown>(pathname: string, options: ApiRequestOptions = {}) => {
34
+ return localCollectionDelete<T>(pathname, options, layout);
35
+ },
36
+ };
37
+ }
38
+
39
+ /*
40
+ export function useCollectionApiFetch<T = unknown>(pathname: string, options: ApiRequestOptions = {}) {
41
+ const layout = useLayout();
42
+ // const layout = { locale: '', market: '' };
43
+ return useHttpFetch<T>(pathname, options, localApiFetch, layout);
44
+ }
45
+ */
46
+
47
+ export function useCollectionApiGet<T = unknown>(pathname: string, params?: QueryParams, options: ApiRequestOptions = {}) {
48
+ const api = useApiFetch<T>(pathname, { ...options, method: 'GET' });
49
+ return api;
50
+ }
51
+
52
+ export function useCollectionApiPost<T = unknown>(pathname: string, payload: unknown, options: ApiRequestOptions = {}) {
53
+ return useApiFetch<T>(pathname, { ...options, method: 'POST', body: payload ? JSON.stringify(payload) : undefined });
54
+ }
55
+
56
+ export function useCollectionApiPut<T = unknown>(pathname: string, payload: unknown, options: ApiRequestOptions = {}) {
57
+ return useApiFetch<T>(pathname, { ...options, method: 'PUT', body: payload ? JSON.stringify(payload) : undefined });
58
+ }
59
+
60
+ export function useCollectionApiPatch<T = unknown>(pathname: string, payload: unknown, options: ApiRequestOptions = {}) {
61
+ return useApiFetch<T>(pathname, { ...options, method: 'PATCH', body: payload ? JSON.stringify(payload) : undefined });
62
+ }
63
+
64
+ export function useCollectionApiDelete<T = unknown>(pathname: string, options: ApiRequestOptions = {}) {
65
+ return useApiFetch<T>(pathname, { ...options, method: 'DELETE' });
66
+ }
@@ -0,0 +1,12 @@
1
+ 'use client';
2
+
3
+ import { useContext } from 'react';
4
+ import { ExtraContext } from './extra';
5
+
6
+ export const useExtra = () => {
7
+ const context = useContext(ExtraContext);
8
+ if (!context) {
9
+ throw new Error('useExtra must be used within ExtraProvider');
10
+ }
11
+ return context;
12
+ };
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+
3
+ import { FetchError, FetchRequestOptions, FetchService, httpFetch } from '@websolutespa/bom-core';
4
+ import { Dispatch, useEffect, useState } from 'react';
5
+
6
+ export function useHttpFetch<T = unknown>(
7
+ pathname: string,
8
+ options: FetchRequestOptions = { method: 'GET' },
9
+ service: FetchService<T> = httpFetch,
10
+ serviceOptions?: unknown
11
+ ): [T | undefined, boolean, FetchError, Dispatch<FetchError>] {
12
+ const [response, setResponse] = useState<T>();
13
+ const [loading, setLoading] = useState<boolean>(false);
14
+ const [error, setError] = useState<FetchError>(null);
15
+ useEffect(() => {
16
+ // const source = axios.CancelToken.source();
17
+ const fetchData = async () => {
18
+ loading ? null : setLoading(true);
19
+ error ? setError(null) : null;
20
+ try {
21
+ const response = await service(pathname, options, serviceOptions);
22
+ // console.log('useHttp.response', response);
23
+ setResponse(response);
24
+ } catch (error: FetchError) {
25
+ // console.error(error)
26
+ setError(error);
27
+ }
28
+ setLoading(false);
29
+ };
30
+ fetchData();
31
+ // return () => source.cancel();
32
+ }, [pathname, options.method, options.body]);
33
+ return [response, loading, error, setError];
34
+ }
35
+
36
+ export function useHttpGet<T = unknown>(pathname: string, options: FetchRequestOptions = {}) {
37
+ return useHttpFetch<T>(pathname, { ...options, method: 'GET' });
38
+ }
39
+
40
+ export function useHttpPost<T = unknown>(pathname: string, payload: unknown, options: FetchRequestOptions = {}) {
41
+ return useHttpFetch<T>(pathname, { ...options, method: 'POST', body: payload ? JSON.stringify(payload) : undefined });
42
+ }
43
+
44
+ export function useHttpPut<T = unknown>(pathname: string, payload: unknown, options: FetchRequestOptions = {}) {
45
+ return useHttpFetch<T>(pathname, { ...options, method: 'PUT', body: payload ? JSON.stringify(payload) : undefined });
46
+ }
47
+
48
+ export function useHttpPatch<T = unknown>(pathname: string, payload: unknown, options: FetchRequestOptions = {}) {
49
+ return useHttpFetch<T>(pathname, { ...options, method: 'PATCH', body: payload ? JSON.stringify(payload) : undefined });
50
+ }
51
+
52
+ export function useHttpDelete<T = unknown>(pathname: string, options: FetchRequestOptions = {}) {
53
+ return useHttpFetch<T>(pathname, { ...options, method: 'DELETE' });
54
+ }
@@ -0,0 +1,76 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { renderHook } from '@testing-library/react';
4
+ import { spyOn } from '@vitest/spy';
5
+ import { ILayout } from '@websolutespa/bom-core';
6
+ import React from 'react';
7
+ import { beforeEach, describe, expect, test } from 'vitest';
8
+ import { interpolateText } from '../lib/label';
9
+ import { LayoutDefaults } from '../lib/layout.defaults';
10
+ import { LabelProvider } from './label';
11
+ import { LayoutProvider } from './layout';
12
+ import { useLabel } from './useLabel';
13
+
14
+ const wrapper = (props: { children: React.ReactNode }) => (
15
+ <LayoutProvider layout={LayoutDefaults as ILayout}>
16
+ <LabelProvider>{props.children}</LabelProvider>
17
+ </LayoutProvider>
18
+ );
19
+
20
+ describe('useLabel', () => {
21
+ beforeEach(() => {
22
+ spyOn(console, 'error').mockImplementation(() => { });
23
+ });
24
+
25
+ test('should throw without provider', () => {
26
+ expect(() => {
27
+ renderHook(() => useLabel());
28
+ }).toThrow('useLabel must be used within LabelProvider');
29
+ });
30
+
31
+ test('should not throw with provider', () => {
32
+ expect(() => {
33
+ renderHook(() => useLabel(), { wrapper });
34
+ }).not.toThrow();
35
+ });
36
+
37
+ test('should not throw with provider null', () => {
38
+ expect(() => {
39
+ renderHook(() => useLabel(), { wrapper });
40
+ }).not.toThrow();
41
+ });
42
+
43
+ test('should return formatted value', () => {
44
+ const { result } = renderHook(() => useLabel(), { wrapper });
45
+ const formatted = result.current('error.required');
46
+ expect(formatted).toBe('Required field');
47
+ });
48
+
49
+ test('non existing key should return key', () => {
50
+ const { result } = renderHook(() => useLabel(), { wrapper });
51
+ const formatted = result.current('not.existing');
52
+ expect(formatted).toBe('not.existing');
53
+ });
54
+
55
+ test('should return formatted value with params', () => {
56
+ const { result } = renderHook(() => useLabel(), { wrapper });
57
+ const formatted = result.current('items.found', { items: 123 });
58
+ expect(formatted).toBe('123 items found');
59
+ });
60
+
61
+ test('value with params without param should return formatted value with token', () => {
62
+ const { result } = renderHook(() => useLabel(), { wrapper });
63
+ const formatted = result.current('items.found');
64
+ expect(formatted).toBe('@items items found');
65
+ });
66
+
67
+ test('interpolateText should format value params', () => {
68
+ const formatted = interpolateText('test @param', { param: 1 });
69
+ expect(formatted).toBe('test 1');
70
+ });
71
+
72
+ test('interpolateText should format value without params', () => {
73
+ const formatted = interpolateText('test @param', {});
74
+ expect(formatted).toBe('test @param');
75
+ });
76
+ });
@@ -0,0 +1,12 @@
1
+ 'use client';
2
+
3
+ import { useContext } from 'react';
4
+ import { LabelContext } from './label';
5
+
6
+ export const useLabel = () => {
7
+ const context = useContext(LabelContext);
8
+ if (!context) {
9
+ throw new Error('useLabel must be used within LabelProvider');
10
+ }
11
+ return context;
12
+ };