@carlonicora/nextjs-jsonapi 1.16.0 → 1.17.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 (123) hide show
  1. package/dist/ApiData-DPKNfY-9.d.mts +10 -0
  2. package/dist/ApiData-DPKNfY-9.d.ts +10 -0
  3. package/dist/ApiRequestDataTypeInterface-DIEOFn9s.d.mts +40 -0
  4. package/dist/ApiRequestDataTypeInterface-DIEOFn9s.d.ts +40 -0
  5. package/dist/{ApiResponseInterface-BvWIeLkq.d.ts → ApiResponseInterface-BKyod24U.d.ts} +2 -11
  6. package/dist/{ApiResponseInterface-CAbw0sv7.d.mts → ApiResponseInterface-Dqvu09tz.d.mts} +2 -11
  7. package/dist/{BlockNoteEditor-MBFDWP7X.js → BlockNoteEditor-34T5CY27.js} +17 -16
  8. package/dist/BlockNoteEditor-34T5CY27.js.map +1 -0
  9. package/dist/{BlockNoteEditor-HFX7Z5BQ.mjs → BlockNoteEditor-4Z6TZBJE.mjs} +7 -6
  10. package/dist/{BlockNoteEditor-HFX7Z5BQ.mjs.map → BlockNoteEditor-4Z6TZBJE.mjs.map} +1 -1
  11. package/dist/JsonApiContext-Bsm_Q2oe.d.mts +41 -0
  12. package/dist/JsonApiContext-Bsm_Q2oe.d.ts +41 -0
  13. package/dist/JsonApiRequest-54ZBO7WQ.js +24 -0
  14. package/dist/{JsonApiRequest-45CLE65I.js.map → JsonApiRequest-54ZBO7WQ.js.map} +1 -1
  15. package/dist/{JsonApiRequest-6IPS3DZJ.mjs → JsonApiRequest-XWQWTFEQ.mjs} +2 -2
  16. package/dist/chunk-3EPNHTMH.js +26 -0
  17. package/dist/chunk-3EPNHTMH.js.map +1 -0
  18. package/dist/{chunk-BCKYJQ3K.mjs → chunk-3VM3WAOV.mjs} +1 -1
  19. package/dist/{chunk-R5QSSISB.js → chunk-7DTKRMYW.js} +21 -14
  20. package/dist/chunk-7DTKRMYW.js.map +1 -0
  21. package/dist/{chunk-ONB2DAIV.js → chunk-D7H7SRWB.js} +455 -470
  22. package/dist/chunk-D7H7SRWB.js.map +1 -0
  23. package/dist/{chunk-BCQSE3EU.mjs → chunk-KUFWHMMY.mjs} +8 -8
  24. package/dist/{chunk-POKIJ56Q.mjs → chunk-KX7YG6LY.mjs} +22 -15
  25. package/dist/chunk-KX7YG6LY.mjs.map +1 -0
  26. package/dist/{chunk-GPGJNTHP.js → chunk-LI6CPNJI.js} +1 -1
  27. package/dist/{chunk-GPGJNTHP.js.map → chunk-LI6CPNJI.js.map} +1 -1
  28. package/dist/{chunk-5RAUCUAA.mjs → chunk-SXPXC2TY.mjs} +18 -33
  29. package/dist/chunk-SXPXC2TY.mjs.map +1 -0
  30. package/dist/{chunk-2AZLCF6D.js → chunk-UYY34W7R.js} +28 -28
  31. package/dist/{chunk-2AZLCF6D.js.map → chunk-UYY34W7R.js.map} +1 -1
  32. package/dist/chunk-VOXD3ZLY.mjs +26 -0
  33. package/dist/chunk-VOXD3ZLY.mjs.map +1 -0
  34. package/dist/client/index.d.mts +11 -45
  35. package/dist/client/index.d.ts +11 -45
  36. package/dist/client/index.js +9 -7
  37. package/dist/client/index.js.map +1 -1
  38. package/dist/client/index.mjs +11 -9
  39. package/dist/components/index.d.mts +4 -3
  40. package/dist/components/index.d.ts +4 -3
  41. package/dist/components/index.js +7 -6
  42. package/dist/components/index.js.map +1 -1
  43. package/dist/components/index.mjs +6 -5
  44. package/dist/{config-DEaUbBqR.d.ts → config--nwiW74Z.d.ts} +1 -1
  45. package/dist/{config-CWsTwnsK.d.mts → config-BKSQmUWU.d.mts} +1 -1
  46. package/dist/{content.interface-D_4b4RQt.d.ts → content.interface-4VICFRA0.d.ts} +2 -1
  47. package/dist/{content.interface-Dk4UZcJM.d.mts → content.interface-CFc97-Cj.d.mts} +2 -1
  48. package/dist/contexts/index.d.mts +3 -2
  49. package/dist/contexts/index.d.ts +3 -2
  50. package/dist/contexts/index.js +7 -6
  51. package/dist/contexts/index.js.map +1 -1
  52. package/dist/contexts/index.mjs +6 -5
  53. package/dist/core/index.d.mts +11 -8
  54. package/dist/core/index.d.ts +11 -8
  55. package/dist/core/index.js +4 -4
  56. package/dist/core/index.js.map +1 -1
  57. package/dist/core/index.mjs +3 -3
  58. package/dist/index.d.mts +15 -11
  59. package/dist/index.d.ts +15 -11
  60. package/dist/index.js +5 -5
  61. package/dist/index.js.map +1 -1
  62. package/dist/index.mjs +6 -6
  63. package/dist/{notification.interface-BllkURRm.d.ts → notification.interface-BGaPiCUM.d.mts} +2 -40
  64. package/dist/{notification.interface-BllkURRm.d.mts → notification.interface-CqwaOIgM.d.ts} +2 -40
  65. package/dist/{s3.service-BEfGqho0.d.ts → s3.service-BYs88XEE.d.ts} +3 -2
  66. package/dist/{s3.service-DIQRYe93.d.mts → s3.service-C0BjOdvn.d.mts} +3 -2
  67. package/dist/server/index.d.mts +6 -4
  68. package/dist/server/index.d.ts +6 -4
  69. package/dist/server/index.js +13 -13
  70. package/dist/server/index.js.map +1 -1
  71. package/dist/server/index.mjs +3 -3
  72. package/dist/{stripe-subscription.interface-C63L6hVg.d.mts → stripe-subscription.interface-B-TM40Io.d.ts} +1 -1
  73. package/dist/{stripe-subscription.interface-CUvNDvw5.d.ts → stripe-subscription.interface-DDxnpj0F.d.mts} +1 -1
  74. package/dist/testing/index.d.mts +338 -0
  75. package/dist/testing/index.d.ts +338 -0
  76. package/dist/testing/index.js +323 -0
  77. package/dist/testing/index.js.map +1 -0
  78. package/dist/testing/index.mjs +323 -0
  79. package/dist/testing/index.mjs.map +1 -0
  80. package/dist/{useSocket-BpenBR2z.d.mts → useSocket-BNj9PrRw.d.mts} +1 -1
  81. package/dist/{useSocket-D-QYA0Sr.d.ts → useSocket-Dwt8cz1x.d.ts} +1 -1
  82. package/package.json +21 -3
  83. package/src/client/hooks/__tests__/useJsonApiGet.test.tsx +229 -0
  84. package/src/client/hooks/__tests__/useJsonApiMutation.test.tsx +348 -0
  85. package/src/client/hooks/__tests__/useRehydration.test.ts +188 -0
  86. package/src/components/forms/__tests__/FormCheckbox.test.tsx +238 -0
  87. package/src/components/forms/__tests__/FormDate.test.tsx +212 -0
  88. package/src/components/forms/__tests__/FormInput.test.tsx +292 -0
  89. package/src/components/forms/__tests__/FormSelect.test.tsx +173 -0
  90. package/src/components/tables/__tests__/ContentListTable.test.tsx +411 -0
  91. package/src/core/endpoint/__tests__/EndpointCreator.test.ts +168 -0
  92. package/src/core/factories/__tests__/JsonApiDataFactory.test.ts +109 -0
  93. package/src/core/factories/__tests__/RehydrationFactory.test.ts +151 -0
  94. package/src/core/registry/__tests__/DataClassRegistry.test.ts +136 -0
  95. package/src/core/registry/__tests__/ModuleRegistrar.test.ts +159 -0
  96. package/src/features/auth/components/details/LandingComponent.tsx +14 -12
  97. package/src/hooks/__tests__/useDataListRetriever.test.ts +321 -0
  98. package/src/hooks/__tests__/useDebounce.test.ts +170 -0
  99. package/src/index.ts +4 -1
  100. package/src/login/config.ts +27 -0
  101. package/src/login/index.ts +2 -0
  102. package/src/testing/factories/createMockApiData.ts +143 -0
  103. package/src/testing/factories/createMockModule.ts +32 -0
  104. package/src/testing/factories/createMockResponse.ts +93 -0
  105. package/src/testing/factories/createMockService.ts +79 -0
  106. package/src/testing/index.ts +70 -0
  107. package/src/testing/matchers/jsonApiMatchers.ts +174 -0
  108. package/src/testing/providers/MockJsonApiProvider.tsx +58 -0
  109. package/src/testing/utils/renderWithProviders.tsx +76 -0
  110. package/src/utils/__tests__/date-formatter.test.ts +161 -0
  111. package/src/utils/__tests__/exists.test.ts +100 -0
  112. package/src/utils/cn.test.ts +44 -0
  113. package/dist/BlockNoteEditor-MBFDWP7X.js.map +0 -1
  114. package/dist/JsonApiRequest-45CLE65I.js +0 -24
  115. package/dist/chunk-5RAUCUAA.mjs.map +0 -1
  116. package/dist/chunk-ONB2DAIV.js.map +0 -1
  117. package/dist/chunk-POKIJ56Q.mjs.map +0 -1
  118. package/dist/chunk-R5QSSISB.js.map +0 -1
  119. package/src/discord/config.ts +0 -15
  120. package/src/discord/index.ts +0 -1
  121. /package/dist/{JsonApiRequest-6IPS3DZJ.mjs.map → JsonApiRequest-XWQWTFEQ.mjs.map} +0 -0
  122. /package/dist/{chunk-BCKYJQ3K.mjs.map → chunk-3VM3WAOV.mjs.map} +0 -0
  123. /package/dist/{chunk-BCQSE3EU.mjs.map → chunk-KUFWHMMY.mjs.map} +0 -0
@@ -0,0 +1,93 @@
1
+ import { ApiResponseInterface } from "../../core/interfaces/ApiResponseInterface";
2
+ import { ApiDataInterface } from "../../core/interfaces/ApiDataInterface";
3
+
4
+ export interface CreateMockResponseOptions {
5
+ data?: ApiDataInterface | ApiDataInterface[] | null;
6
+ ok?: boolean;
7
+ response?: number;
8
+ error?: string;
9
+ meta?: Record<string, any>;
10
+ self?: string;
11
+ next?: string;
12
+ prev?: string;
13
+ }
14
+
15
+ /**
16
+ * Creates a mock API response for testing.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { createMockResponse, createMockApiData } from '@carlonicora/nextjs-jsonapi/testing';
21
+ *
22
+ * const mockData = createMockApiData({ type: 'articles', id: '1' });
23
+ * const response = createMockResponse({ data: mockData, ok: true });
24
+ * ```
25
+ *
26
+ * @example With pagination
27
+ * ```ts
28
+ * const response = createMockResponse({
29
+ * data: [mockData],
30
+ * ok: true,
31
+ * next: '/articles?page=2',
32
+ * prev: '/articles?page=0',
33
+ * });
34
+ * ```
35
+ */
36
+ export function createMockResponse(options: CreateMockResponseOptions = {}): ApiResponseInterface {
37
+ const {
38
+ data = null,
39
+ ok = true,
40
+ response = ok ? 200 : 500,
41
+ error = ok ? "" : "Error",
42
+ meta,
43
+ self,
44
+ next,
45
+ prev,
46
+ } = options;
47
+
48
+ const mockResponse: ApiResponseInterface = {
49
+ ok,
50
+ response,
51
+ data: data as ApiDataInterface | ApiDataInterface[],
52
+ error,
53
+ meta,
54
+ self,
55
+ next,
56
+ prev,
57
+ };
58
+
59
+ // Add pagination methods if next/prev provided
60
+ if (next) {
61
+ mockResponse.nextPage = async () =>
62
+ createMockResponse({ ...options, next: undefined, prev: self });
63
+ }
64
+
65
+ if (prev) {
66
+ mockResponse.prevPage = async () =>
67
+ createMockResponse({ ...options, prev: undefined, next: self });
68
+ }
69
+
70
+ return mockResponse;
71
+ }
72
+
73
+ /**
74
+ * Creates a mock error response for testing error scenarios.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * import { createMockErrorResponse } from '@carlonicora/nextjs-jsonapi/testing';
79
+ *
80
+ * const errorResponse = createMockErrorResponse(404, 'Not Found');
81
+ * ```
82
+ */
83
+ export function createMockErrorResponse(
84
+ statusCode: number,
85
+ errorMessage: string
86
+ ): ApiResponseInterface {
87
+ return createMockResponse({
88
+ ok: false,
89
+ response: statusCode,
90
+ error: errorMessage,
91
+ data: null,
92
+ });
93
+ }
@@ -0,0 +1,79 @@
1
+ import { vi, type Mock } from "vitest";
2
+ import { ApiResponseInterface } from "../../core/interfaces/ApiResponseInterface";
3
+ import { createMockResponse } from "./createMockResponse";
4
+
5
+ export type MockApiMethod = Mock<(...args: any[]) => Promise<ApiResponseInterface>>;
6
+
7
+ export interface MockService {
8
+ get: MockApiMethod;
9
+ post: MockApiMethod;
10
+ put: MockApiMethod;
11
+ patch: MockApiMethod;
12
+ delete: MockApiMethod;
13
+ }
14
+
15
+ export interface CreateMockServiceOptions {
16
+ defaultResponse?: ApiResponseInterface;
17
+ }
18
+
19
+ /**
20
+ * Creates a mock service with Vitest mock functions for all HTTP methods.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { createMockService, createMockResponse } from '@carlonicora/nextjs-jsonapi/testing';
25
+ *
26
+ * const mockService = createMockService();
27
+ * mockService.get.mockResolvedValue(createMockResponse({ data: mockData }));
28
+ *
29
+ * // Use in test
30
+ * expect(mockService.get).toHaveBeenCalled();
31
+ * ```
32
+ *
33
+ * @example With default response
34
+ * ```ts
35
+ * const mockService = createMockService({
36
+ * defaultResponse: createMockResponse({ ok: true, data: [] }),
37
+ * });
38
+ * ```
39
+ */
40
+ export function createMockService(options: CreateMockServiceOptions = {}): MockService {
41
+ const defaultResponse = options.defaultResponse ?? createMockResponse({ ok: true });
42
+
43
+ return {
44
+ get: vi.fn().mockResolvedValue(defaultResponse),
45
+ post: vi.fn().mockResolvedValue(defaultResponse),
46
+ put: vi.fn().mockResolvedValue(defaultResponse),
47
+ patch: vi.fn().mockResolvedValue(defaultResponse),
48
+ delete: vi.fn().mockResolvedValue(defaultResponse),
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Creates a mock service that returns errors for all methods.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * import { createMockErrorService } from '@carlonicora/nextjs-jsonapi/testing';
58
+ *
59
+ * const errorService = createMockErrorService(500, 'Internal Server Error');
60
+ * ```
61
+ */
62
+ export function createMockErrorService(
63
+ statusCode: number = 500,
64
+ errorMessage: string = "Error"
65
+ ): MockService {
66
+ const errorResponse = createMockResponse({
67
+ ok: false,
68
+ response: statusCode,
69
+ error: errorMessage,
70
+ });
71
+
72
+ return {
73
+ get: vi.fn().mockResolvedValue(errorResponse),
74
+ post: vi.fn().mockResolvedValue(errorResponse),
75
+ put: vi.fn().mockResolvedValue(errorResponse),
76
+ patch: vi.fn().mockResolvedValue(errorResponse),
77
+ delete: vi.fn().mockResolvedValue(errorResponse),
78
+ };
79
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Test utilities for @carlonicora/nextjs-jsonapi
3
+ *
4
+ * Import from '@carlonicora/nextjs-jsonapi/testing'
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import {
9
+ * MockJsonApiProvider,
10
+ * renderWithProviders,
11
+ * createMockApiData,
12
+ * createMockResponse,
13
+ * createMockModule,
14
+ * createMockService,
15
+ * jsonApiMatchers,
16
+ * extendExpectWithJsonApiMatchers,
17
+ * } from '@carlonicora/nextjs-jsonapi/testing';
18
+ * ```
19
+ */
20
+
21
+ // Providers
22
+ export {
23
+ MockJsonApiProvider,
24
+ defaultMockConfig,
25
+ type MockJsonApiProviderProps,
26
+ } from "./providers/MockJsonApiProvider";
27
+
28
+ // Factories
29
+ export {
30
+ createMockModule,
31
+ type CreateMockModuleOptions,
32
+ } from "./factories/createMockModule";
33
+
34
+ export {
35
+ createMockResponse,
36
+ createMockErrorResponse,
37
+ type CreateMockResponseOptions,
38
+ } from "./factories/createMockResponse";
39
+
40
+ export {
41
+ createMockService,
42
+ createMockErrorService,
43
+ type MockService,
44
+ type MockApiMethod,
45
+ type CreateMockServiceOptions,
46
+ } from "./factories/createMockService";
47
+
48
+ export {
49
+ createMockApiData,
50
+ createMockApiDataList,
51
+ type CreateMockApiDataOptions,
52
+ } from "./factories/createMockApiData";
53
+
54
+ // Matchers
55
+ export {
56
+ jsonApiMatchers,
57
+ extendExpectWithJsonApiMatchers,
58
+ } from "./matchers/jsonApiMatchers";
59
+
60
+ // Utilities
61
+ export {
62
+ renderWithProviders,
63
+ render,
64
+ screen,
65
+ waitFor,
66
+ fireEvent,
67
+ within,
68
+ userEvent,
69
+ type RenderWithProvidersOptions,
70
+ } from "./utils/renderWithProviders";
@@ -0,0 +1,174 @@
1
+ import { expect } from "vitest";
2
+
3
+ interface JsonApiResponse {
4
+ data?: {
5
+ type?: string;
6
+ id?: string;
7
+ attributes?: Record<string, any>;
8
+ relationships?: Record<string, any>;
9
+ } | Array<{
10
+ type?: string;
11
+ id?: string;
12
+ attributes?: Record<string, any>;
13
+ relationships?: Record<string, any>;
14
+ }>;
15
+ }
16
+
17
+ /**
18
+ * Custom Vitest matchers for JSON:API assertions.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { jsonApiMatchers } from '@carlonicora/nextjs-jsonapi/testing';
23
+ * import { expect } from 'vitest';
24
+ *
25
+ * expect.extend(jsonApiMatchers);
26
+ *
27
+ * // Then use in tests:
28
+ * expect(response).toBeValidJsonApi();
29
+ * expect(response).toHaveJsonApiType('articles');
30
+ * expect(response).toHaveJsonApiAttribute('title', 'My Article');
31
+ * expect(response).toHaveJsonApiRelationship('author');
32
+ * ```
33
+ */
34
+ export const jsonApiMatchers = {
35
+ /**
36
+ * Asserts that the response has a valid JSON:API structure with type and id.
37
+ */
38
+ toBeValidJsonApi(received: JsonApiResponse) {
39
+ const data = Array.isArray(received?.data) ? received.data[0] : received?.data;
40
+ const hasType = typeof data?.type === "string" && data.type.length > 0;
41
+ const hasId = typeof data?.id === "string" && data.id.length > 0;
42
+ const isValid = hasType && hasId;
43
+
44
+ return {
45
+ pass: isValid,
46
+ message: () =>
47
+ isValid
48
+ ? `Expected response not to be valid JSON:API, but it has type "${data?.type}" and id "${data?.id}"`
49
+ : `Expected response to be valid JSON:API with type and id, but got type: ${JSON.stringify(data?.type)}, id: ${JSON.stringify(data?.id)}`,
50
+ };
51
+ },
52
+
53
+ /**
54
+ * Asserts that the response data has the expected JSON:API type.
55
+ */
56
+ toHaveJsonApiType(received: JsonApiResponse, expectedType: string) {
57
+ const data = Array.isArray(received?.data) ? received.data[0] : received?.data;
58
+ const actualType = data?.type;
59
+ const pass = actualType === expectedType;
60
+
61
+ return {
62
+ pass,
63
+ message: () =>
64
+ pass
65
+ ? `Expected response not to have JSON:API type "${expectedType}"`
66
+ : `Expected response to have JSON:API type "${expectedType}", but got "${actualType}"`,
67
+ };
68
+ },
69
+
70
+ /**
71
+ * Asserts that the response data has an attribute with the expected value.
72
+ */
73
+ toHaveJsonApiAttribute(
74
+ received: JsonApiResponse,
75
+ attributeName: string,
76
+ expectedValue?: any
77
+ ) {
78
+ const data = Array.isArray(received?.data) ? received.data[0] : received?.data;
79
+ const attributes = data?.attributes ?? {};
80
+ const hasAttribute = attributeName in attributes;
81
+ const actualValue = attributes[attributeName];
82
+
83
+ if (expectedValue === undefined) {
84
+ // Just check existence
85
+ return {
86
+ pass: hasAttribute,
87
+ message: () =>
88
+ hasAttribute
89
+ ? `Expected response not to have JSON:API attribute "${attributeName}"`
90
+ : `Expected response to have JSON:API attribute "${attributeName}", but it was not found. Available attributes: ${Object.keys(attributes).join(", ") || "none"}`,
91
+ };
92
+ }
93
+
94
+ const valuesMatch = actualValue === expectedValue;
95
+ return {
96
+ pass: hasAttribute && valuesMatch,
97
+ message: () =>
98
+ hasAttribute && valuesMatch
99
+ ? `Expected response not to have JSON:API attribute "${attributeName}" with value "${expectedValue}"`
100
+ : !hasAttribute
101
+ ? `Expected response to have JSON:API attribute "${attributeName}", but it was not found`
102
+ : `Expected JSON:API attribute "${attributeName}" to be "${expectedValue}", but got "${actualValue}"`,
103
+ };
104
+ },
105
+
106
+ /**
107
+ * Asserts that the response data has the specified relationship.
108
+ */
109
+ toHaveJsonApiRelationship(received: JsonApiResponse, relationshipName: string) {
110
+ const data = Array.isArray(received?.data) ? received.data[0] : received?.data;
111
+ const relationships = data?.relationships ?? {};
112
+ const hasRelationship = relationshipName in relationships;
113
+
114
+ return {
115
+ pass: hasRelationship,
116
+ message: () =>
117
+ hasRelationship
118
+ ? `Expected response not to have JSON:API relationship "${relationshipName}"`
119
+ : `Expected response to have JSON:API relationship "${relationshipName}", but it was not found. Available relationships: ${Object.keys(relationships).join(", ") || "none"}`,
120
+ };
121
+ },
122
+
123
+ /**
124
+ * Asserts that the response data array has the expected length.
125
+ */
126
+ toHaveJsonApiLength(received: JsonApiResponse, expectedLength: number) {
127
+ const data = received?.data;
128
+ const isArray = Array.isArray(data);
129
+ const actualLength = isArray ? data.length : data ? 1 : 0;
130
+ const pass = actualLength === expectedLength;
131
+
132
+ return {
133
+ pass,
134
+ message: () =>
135
+ pass
136
+ ? `Expected response not to have ${expectedLength} items`
137
+ : `Expected response to have ${expectedLength} items, but got ${actualLength}`,
138
+ };
139
+ },
140
+ };
141
+
142
+ // Type declarations for the custom matchers
143
+ declare module "vitest" {
144
+ interface Assertion<T = any> {
145
+ toBeValidJsonApi(): T;
146
+ toHaveJsonApiType(expectedType: string): T;
147
+ toHaveJsonApiAttribute(attributeName: string, expectedValue?: any): T;
148
+ toHaveJsonApiRelationship(relationshipName: string): T;
149
+ toHaveJsonApiLength(expectedLength: number): T;
150
+ }
151
+ interface AsymmetricMatchersContaining {
152
+ toBeValidJsonApi(): any;
153
+ toHaveJsonApiType(expectedType: string): any;
154
+ toHaveJsonApiAttribute(attributeName: string, expectedValue?: any): any;
155
+ toHaveJsonApiRelationship(relationshipName: string): any;
156
+ toHaveJsonApiLength(expectedLength: number): any;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Extends Vitest's expect with JSON:API matchers.
162
+ * Call this in your test setup file.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * // vitest.setup.ts
167
+ * import { extendExpectWithJsonApiMatchers } from '@carlonicora/nextjs-jsonapi/testing';
168
+ *
169
+ * extendExpectWithJsonApiMatchers();
170
+ * ```
171
+ */
172
+ export function extendExpectWithJsonApiMatchers(): void {
173
+ expect.extend(jsonApiMatchers);
174
+ }
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { JsonApiConfig, JsonApiContext } from "../../client/context/JsonApiContext";
5
+
6
+ export interface MockJsonApiProviderProps {
7
+ children: React.ReactNode;
8
+ config?: Partial<JsonApiConfig>;
9
+ }
10
+
11
+ const defaultMockConfig: JsonApiConfig = {
12
+ apiUrl: "https://api.test.com",
13
+ tokenGetter: async () => "mock-token-for-testing",
14
+ languageGetter: async () => "en",
15
+ defaultHeaders: {},
16
+ onError: () => {},
17
+ cacheConfig: {
18
+ defaultProfile: "default",
19
+ },
20
+ };
21
+
22
+ /**
23
+ * A test-friendly provider that wraps components with mock JSON:API context.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * import { MockJsonApiProvider } from '@carlonicora/nextjs-jsonapi/testing';
28
+ *
29
+ * render(
30
+ * <MockJsonApiProvider>
31
+ * <MyComponent />
32
+ * </MockJsonApiProvider>
33
+ * );
34
+ * ```
35
+ *
36
+ * @example With custom config
37
+ * ```tsx
38
+ * render(
39
+ * <MockJsonApiProvider config={{ apiUrl: 'https://custom.api.com' }}>
40
+ * <MyComponent />
41
+ * </MockJsonApiProvider>
42
+ * );
43
+ * ```
44
+ */
45
+ export function MockJsonApiProvider({ children, config }: MockJsonApiProviderProps) {
46
+ const mergedConfig: JsonApiConfig = {
47
+ ...defaultMockConfig,
48
+ ...config,
49
+ };
50
+
51
+ return (
52
+ <JsonApiContext.Provider value={mergedConfig}>
53
+ {children}
54
+ </JsonApiContext.Provider>
55
+ );
56
+ }
57
+
58
+ export { defaultMockConfig };
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ import React, { ReactElement } from "react";
4
+ import { render, RenderOptions, RenderResult } from "@testing-library/react";
5
+ import { MockJsonApiProvider, MockJsonApiProviderProps } from "../providers/MockJsonApiProvider";
6
+ import { JsonApiConfig } from "../../client/context/JsonApiContext";
7
+
8
+ export interface RenderWithProvidersOptions extends Omit<RenderOptions, "wrapper"> {
9
+ /**
10
+ * Custom JSON:API configuration to pass to the mock provider.
11
+ */
12
+ jsonApiConfig?: Partial<JsonApiConfig>;
13
+
14
+ /**
15
+ * Additional wrapper component to wrap around the providers.
16
+ */
17
+ wrapper?: React.ComponentType<{ children: React.ReactNode }>;
18
+ }
19
+
20
+ /**
21
+ * Renders a component wrapped with all necessary providers for testing.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * import { renderWithProviders } from '@carlonicora/nextjs-jsonapi/testing';
26
+ *
27
+ * const { getByText } = renderWithProviders(<MyComponent />);
28
+ * expect(getByText('Hello')).toBeInTheDocument();
29
+ * ```
30
+ *
31
+ * @example With custom config
32
+ * ```tsx
33
+ * const { getByText } = renderWithProviders(<MyComponent />, {
34
+ * jsonApiConfig: { apiUrl: 'https://custom.api.com' },
35
+ * });
36
+ * ```
37
+ *
38
+ * @example With additional wrapper
39
+ * ```tsx
40
+ * const CustomWrapper = ({ children }) => (
41
+ * <ThemeProvider>{children}</ThemeProvider>
42
+ * );
43
+ *
44
+ * const { getByText } = renderWithProviders(<MyComponent />, {
45
+ * wrapper: CustomWrapper,
46
+ * });
47
+ * ```
48
+ */
49
+ export function renderWithProviders(
50
+ ui: ReactElement,
51
+ options: RenderWithProvidersOptions = {}
52
+ ): RenderResult {
53
+ const { jsonApiConfig, wrapper: AdditionalWrapper, ...renderOptions } = options;
54
+
55
+ function AllProviders({ children }: { children: React.ReactNode }) {
56
+ const content = (
57
+ <MockJsonApiProvider config={jsonApiConfig}>
58
+ {children}
59
+ </MockJsonApiProvider>
60
+ );
61
+
62
+ if (AdditionalWrapper) {
63
+ return <AdditionalWrapper>{content}</AdditionalWrapper>;
64
+ }
65
+
66
+ return content;
67
+ }
68
+
69
+ return render(ui, { wrapper: AllProviders, ...renderOptions });
70
+ }
71
+
72
+ /**
73
+ * Re-export render utilities from Testing Library for convenience.
74
+ */
75
+ export { render, screen, waitFor, fireEvent, within } from "@testing-library/react";
76
+ export { userEvent } from "@testing-library/user-event";