@multiplatform.one/test-utils 6.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.
- package/LICENSE +201 -0
- package/package.json +76 -0
- package/src/index.tsx +387 -0
- package/src/mocks.ts +245 -0
- package/src/setup.ts +650 -0
package/src/setup.ts
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test setup for multiplatform.one packages.
|
|
3
|
+
*
|
|
4
|
+
* Use this as a vitest setupFile to get all the polyfills, mocks, and
|
|
5
|
+
* environment patches needed for testing Tamagui + React Native Web components.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // vitest.config.mjs
|
|
10
|
+
* import { createVitestConfig } from "@multiplatform.one/config/vitest";
|
|
11
|
+
* export default createVitestConfig({
|
|
12
|
+
* setupFiles: ["@multiplatform.one/test-utils/setup"],
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// localStorage polyfill FIRST so Tamagui plugin (tamagui-extract) can run during transform
|
|
18
|
+
const _noopStorage = {
|
|
19
|
+
getItem: () => null,
|
|
20
|
+
setItem: () => {},
|
|
21
|
+
removeItem: () => {},
|
|
22
|
+
clear: () => {},
|
|
23
|
+
get length() {
|
|
24
|
+
return 0;
|
|
25
|
+
},
|
|
26
|
+
key: () => null,
|
|
27
|
+
};
|
|
28
|
+
if (typeof globalThis !== "undefined" && typeof (globalThis as any).localStorage === "undefined") {
|
|
29
|
+
Object.defineProperty(globalThis, "localStorage", { value: _noopStorage, writable: true });
|
|
30
|
+
}
|
|
31
|
+
if (typeof global !== "undefined" && !("localStorage" in global)) {
|
|
32
|
+
Object.defineProperty(global, "localStorage", { value: _noopStorage, writable: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// DOM polyfills -- must run BEFORE any imports that use them
|
|
36
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
37
|
+
// happy-dom exposes CSSStyleDeclaration index properties as getter-only.
|
|
38
|
+
// Some RN-web/Tamagui render paths can transiently pass array-like style maps
|
|
39
|
+
// during test rendering, which makes React assign style[0], style[1], etc.
|
|
40
|
+
// Defining no-op numeric setters prevents hard crashes in tests.
|
|
41
|
+
if (typeof CSSStyleDeclaration !== "undefined" && CSSStyleDeclaration.prototype) {
|
|
42
|
+
for (let i = 0; i <= 64; i++) {
|
|
43
|
+
const key = String(i);
|
|
44
|
+
const desc = Object.getOwnPropertyDescriptor(CSSStyleDeclaration.prototype, key);
|
|
45
|
+
if (!desc || desc.set == null) {
|
|
46
|
+
Object.defineProperty(CSSStyleDeclaration.prototype, key, {
|
|
47
|
+
configurable: true,
|
|
48
|
+
enumerable: false,
|
|
49
|
+
get() {
|
|
50
|
+
return "";
|
|
51
|
+
},
|
|
52
|
+
set() {
|
|
53
|
+
// no-op in test environment
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fix NodeList.prototype to ensure forEach exists
|
|
61
|
+
if (typeof NodeList !== "undefined" && NodeList.prototype) {
|
|
62
|
+
if (!NodeList.prototype.forEach) {
|
|
63
|
+
(NodeList.prototype as any).forEach = Array.prototype.forEach;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Also add to HTMLCollection
|
|
68
|
+
if (typeof HTMLCollection !== "undefined" && HTMLCollection.prototype) {
|
|
69
|
+
if (!(HTMLCollection.prototype as any).forEach) {
|
|
70
|
+
(HTMLCollection.prototype as any).forEach = Array.prototype.forEach;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Element.matches polyfill
|
|
75
|
+
const matchesImpl = function (this: any, selector: string) {
|
|
76
|
+
if (!this || typeof this !== "object") return false;
|
|
77
|
+
if (this.nodeType === 1) {
|
|
78
|
+
if (this.webkitMatchesSelector) return this.webkitMatchesSelector(selector);
|
|
79
|
+
if (this.mozMatchesSelector) return this.mozMatchesSelector(selector);
|
|
80
|
+
if (this.msMatchesSelector) return this.msMatchesSelector(selector);
|
|
81
|
+
const matches = (this.ownerDocument || document).querySelectorAll(selector);
|
|
82
|
+
let i = matches.length;
|
|
83
|
+
while (--i >= 0 && matches[i] !== this) {}
|
|
84
|
+
return i > -1;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (Element.prototype && !Element.prototype.matches) {
|
|
90
|
+
Object.defineProperty(Element.prototype, "matches", {
|
|
91
|
+
value: matchesImpl,
|
|
92
|
+
writable: true,
|
|
93
|
+
configurable: true,
|
|
94
|
+
enumerable: false,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
typeof HTMLElement !== "undefined" &&
|
|
100
|
+
HTMLElement.prototype &&
|
|
101
|
+
!HTMLElement.prototype.matches
|
|
102
|
+
) {
|
|
103
|
+
Object.defineProperty(HTMLElement.prototype, "matches", {
|
|
104
|
+
value: matchesImpl,
|
|
105
|
+
writable: true,
|
|
106
|
+
configurable: true,
|
|
107
|
+
enumerable: false,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// hasAttribute / getAttribute / setAttribute polyfills for all Node types
|
|
112
|
+
const hasAttributeImpl = function (this: any, name: string): boolean {
|
|
113
|
+
if (!this || this.nodeType !== 1) return false;
|
|
114
|
+
if (Element.prototype.hasAttribute && this instanceof Element) {
|
|
115
|
+
return Element.prototype.hasAttribute.call(this, name);
|
|
116
|
+
}
|
|
117
|
+
if (this.attributes) {
|
|
118
|
+
for (let i = 0; i < this.attributes.length; i++) {
|
|
119
|
+
if (this.attributes[i].name === name) return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const getAttributeImpl = function (this: any, name: string): string | null {
|
|
126
|
+
if (!this || this.nodeType !== 1) return null;
|
|
127
|
+
if (Element.prototype.getAttribute && this instanceof Element) {
|
|
128
|
+
return Element.prototype.getAttribute.call(this, name);
|
|
129
|
+
}
|
|
130
|
+
if (this.attributes) {
|
|
131
|
+
for (let i = 0; i < this.attributes.length; i++) {
|
|
132
|
+
if (this.attributes[i].name === name) return this.attributes[i].value;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const nodeTypes = (
|
|
139
|
+
[
|
|
140
|
+
typeof Node !== "undefined" && Node,
|
|
141
|
+
typeof Element !== "undefined" && Element,
|
|
142
|
+
typeof HTMLElement !== "undefined" && HTMLElement,
|
|
143
|
+
typeof HTMLDivElement !== "undefined" && HTMLDivElement,
|
|
144
|
+
typeof HTMLSpanElement !== "undefined" && HTMLSpanElement,
|
|
145
|
+
typeof HTMLInputElement !== "undefined" && HTMLInputElement,
|
|
146
|
+
typeof HTMLButtonElement !== "undefined" && HTMLButtonElement,
|
|
147
|
+
typeof HTMLFormElement !== "undefined" && HTMLFormElement,
|
|
148
|
+
typeof HTMLLabelElement !== "undefined" && HTMLLabelElement,
|
|
149
|
+
typeof Text !== "undefined" && Text,
|
|
150
|
+
typeof Comment !== "undefined" && Comment,
|
|
151
|
+
typeof Document !== "undefined" && Document,
|
|
152
|
+
typeof DocumentFragment !== "undefined" && DocumentFragment,
|
|
153
|
+
] as Array<false | (new (...args: any[]) => any)>
|
|
154
|
+
).filter(Boolean) as Array<new (...args: any[]) => any>;
|
|
155
|
+
|
|
156
|
+
for (const NodeType of nodeTypes) {
|
|
157
|
+
if ((NodeType as any)?.prototype) {
|
|
158
|
+
if (!(NodeType as any).prototype.hasAttribute) {
|
|
159
|
+
Object.defineProperty((NodeType as any).prototype, "hasAttribute", {
|
|
160
|
+
value: hasAttributeImpl,
|
|
161
|
+
writable: true,
|
|
162
|
+
configurable: true,
|
|
163
|
+
enumerable: false,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (!(NodeType as any).prototype.getAttribute) {
|
|
167
|
+
Object.defineProperty((NodeType as any).prototype, "getAttribute", {
|
|
168
|
+
value: getAttributeImpl,
|
|
169
|
+
writable: true,
|
|
170
|
+
configurable: true,
|
|
171
|
+
enumerable: false,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (!(NodeType as any).prototype.setAttribute) {
|
|
175
|
+
Object.defineProperty((NodeType as any).prototype, "setAttribute", {
|
|
176
|
+
value: function (name: string, value: string) {
|
|
177
|
+
if (this.nodeType === 1 && Element.prototype.setAttribute) {
|
|
178
|
+
Element.prototype.setAttribute.call(this, name, value);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
writable: true,
|
|
182
|
+
configurable: true,
|
|
183
|
+
enumerable: false,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Polyfill HTMLFormElement.prototype.requestSubmit for jsdom
|
|
190
|
+
if (typeof HTMLFormElement !== "undefined") {
|
|
191
|
+
Object.defineProperty(HTMLFormElement.prototype, "requestSubmit", {
|
|
192
|
+
value: function (submitter?: HTMLElement) {
|
|
193
|
+
if (submitter) {
|
|
194
|
+
if ((submitter as HTMLButtonElement).type !== "submit") {
|
|
195
|
+
throw new TypeError("The specified element is not a submit button");
|
|
196
|
+
}
|
|
197
|
+
if ((submitter as HTMLButtonElement).form !== this) {
|
|
198
|
+
throw new DOMException(
|
|
199
|
+
"The specified element is not owned by this form element",
|
|
200
|
+
"NotFoundError",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const submitEvent = new Event("submit", { bubbles: true, cancelable: true });
|
|
205
|
+
this.dispatchEvent(submitEvent);
|
|
206
|
+
},
|
|
207
|
+
writable: true,
|
|
208
|
+
configurable: true,
|
|
209
|
+
enumerable: false,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Now safe to import modules that depend on polyfills
|
|
215
|
+
import "@testing-library/jest-dom";
|
|
216
|
+
import "matchmedia-polyfill";
|
|
217
|
+
import { config } from "@tamagui/config";
|
|
218
|
+
import { cleanup } from "@testing-library/react";
|
|
219
|
+
import { createTamagui } from "tamagui";
|
|
220
|
+
import { afterEach, beforeEach, vi } from "vitest";
|
|
221
|
+
|
|
222
|
+
// Patch @floating-ui/react to handle missing forEach in test environment
|
|
223
|
+
try {
|
|
224
|
+
const floatingUiUtils = await import("@floating-ui/react/utils");
|
|
225
|
+
if (floatingUiUtils?.enableFocusInside) {
|
|
226
|
+
const originalEnableFocusInside = floatingUiUtils.enableFocusInside;
|
|
227
|
+
(floatingUiUtils as any).enableFocusInside = (container: any) => {
|
|
228
|
+
if (!container || !container.querySelectorAll) return;
|
|
229
|
+
const elements = container.querySelectorAll("[data-tabindex]");
|
|
230
|
+
if (!elements.forEach) {
|
|
231
|
+
(elements as any).forEach = Array.prototype.forEach;
|
|
232
|
+
}
|
|
233
|
+
return originalEnableFocusInside(container);
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// Ignore if module not found
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Initialize Tamagui with config
|
|
241
|
+
createTamagui(config);
|
|
242
|
+
|
|
243
|
+
// No-op localStorage for Node/vitest
|
|
244
|
+
const noopStorage = {
|
|
245
|
+
getItem: () => null,
|
|
246
|
+
setItem: () => {},
|
|
247
|
+
removeItem: () => {},
|
|
248
|
+
clear: () => {},
|
|
249
|
+
get length() {
|
|
250
|
+
return 0;
|
|
251
|
+
},
|
|
252
|
+
key: () => null,
|
|
253
|
+
};
|
|
254
|
+
vi.stubGlobal("localStorage", noopStorage);
|
|
255
|
+
Object.defineProperty(globalThis, "localStorage", { value: noopStorage, writable: true });
|
|
256
|
+
if (typeof global !== "undefined") {
|
|
257
|
+
Object.defineProperty(global, "localStorage", { value: noopStorage, writable: true });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// BroadcastChannel mock
|
|
261
|
+
if (
|
|
262
|
+
typeof globalThis !== "undefined" &&
|
|
263
|
+
typeof (globalThis as any).BroadcastChannel === "undefined"
|
|
264
|
+
) {
|
|
265
|
+
(globalThis as any).BroadcastChannel = class BroadcastChannelMock {
|
|
266
|
+
name: string;
|
|
267
|
+
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
268
|
+
constructor(name: string) {
|
|
269
|
+
this.name = name;
|
|
270
|
+
}
|
|
271
|
+
postMessage(_message: any) {}
|
|
272
|
+
close() {}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// IndexedDB mock
|
|
277
|
+
if (typeof window !== "undefined" && !window.indexedDB) {
|
|
278
|
+
Object.defineProperty(window, "indexedDB", {
|
|
279
|
+
value: {
|
|
280
|
+
open: () => ({ onsuccess: null, onerror: null, onupgradeneeded: null, result: null }),
|
|
281
|
+
deleteDatabase: () => ({ onsuccess: null, onerror: null }),
|
|
282
|
+
},
|
|
283
|
+
writable: true,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Stub fetch to prevent real network calls
|
|
288
|
+
vi.stubGlobal(
|
|
289
|
+
"fetch",
|
|
290
|
+
vi.fn(() =>
|
|
291
|
+
Promise.resolve(
|
|
292
|
+
new Response(JSON.stringify({ data: [] }), {
|
|
293
|
+
status: 200,
|
|
294
|
+
headers: { "Content-Type": "application/json" },
|
|
295
|
+
}),
|
|
296
|
+
),
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// matchMedia polyfill
|
|
301
|
+
window.matchMedia =
|
|
302
|
+
window.matchMedia ||
|
|
303
|
+
(() => ({
|
|
304
|
+
matches: false,
|
|
305
|
+
addListener: () => {},
|
|
306
|
+
removeListener: () => {},
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
// requestAnimationFrame / cancelAnimationFrame polyfill
|
|
310
|
+
global.requestAnimationFrame = ((callback: FrameRequestCallback): number => {
|
|
311
|
+
return setTimeout(callback, 0) as unknown as number;
|
|
312
|
+
}) as typeof window.requestAnimationFrame;
|
|
313
|
+
|
|
314
|
+
global.cancelAnimationFrame = (id: number): void => {
|
|
315
|
+
clearTimeout(id);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// ResizeObserver mock
|
|
319
|
+
class MockResizeObserver {
|
|
320
|
+
observe() {}
|
|
321
|
+
unobserve() {}
|
|
322
|
+
disconnect() {}
|
|
323
|
+
}
|
|
324
|
+
window.ResizeObserver = MockResizeObserver as any;
|
|
325
|
+
|
|
326
|
+
// IntersectionObserver mock
|
|
327
|
+
class MockIntersectionObserver implements IntersectionObserver {
|
|
328
|
+
readonly root: Element | null = null;
|
|
329
|
+
readonly rootMargin: string = "0px";
|
|
330
|
+
readonly thresholds: ReadonlyArray<number> = [0];
|
|
331
|
+
observe(): void {}
|
|
332
|
+
unobserve(): void {}
|
|
333
|
+
disconnect(): void {}
|
|
334
|
+
takeRecords(): IntersectionObserverEntry[] {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (typeof window !== "undefined") {
|
|
339
|
+
Object.defineProperty(window, "IntersectionObserver", {
|
|
340
|
+
value: MockIntersectionObserver,
|
|
341
|
+
writable: true,
|
|
342
|
+
configurable: true,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
Object.defineProperty(globalThis, "IntersectionObserver", {
|
|
346
|
+
value: MockIntersectionObserver,
|
|
347
|
+
writable: true,
|
|
348
|
+
configurable: true,
|
|
349
|
+
});
|
|
350
|
+
if (typeof global !== "undefined") {
|
|
351
|
+
Object.defineProperty(global, "IntersectionObserver", {
|
|
352
|
+
value: MockIntersectionObserver,
|
|
353
|
+
writable: true,
|
|
354
|
+
configurable: true,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (typeof self !== "undefined") {
|
|
358
|
+
Object.defineProperty(self, "IntersectionObserver", {
|
|
359
|
+
value: MockIntersectionObserver,
|
|
360
|
+
writable: true,
|
|
361
|
+
configurable: true,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// DataTransfer mock
|
|
366
|
+
if (typeof DataTransfer === "undefined") {
|
|
367
|
+
(global as any).DataTransfer = class DataTransfer {
|
|
368
|
+
private data: Map<string, string>;
|
|
369
|
+
constructor() {
|
|
370
|
+
this.data = new Map();
|
|
371
|
+
}
|
|
372
|
+
getData(format: string): string {
|
|
373
|
+
return this.data.get(format) || "";
|
|
374
|
+
}
|
|
375
|
+
setData(format: string, data: string): void {
|
|
376
|
+
this.data.set(format, data);
|
|
377
|
+
}
|
|
378
|
+
clearData(format?: string): void {
|
|
379
|
+
if (format) {
|
|
380
|
+
this.data.delete(format);
|
|
381
|
+
} else {
|
|
382
|
+
this.data.clear();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ClipboardEvent mock
|
|
389
|
+
if (typeof ClipboardEvent === "undefined") {
|
|
390
|
+
(global as any).ClipboardEvent = class ClipboardEvent extends Event {
|
|
391
|
+
clipboardData: any;
|
|
392
|
+
constructor(type: string, options?: any) {
|
|
393
|
+
super(type, options);
|
|
394
|
+
this.clipboardData = options?.clipboardData || {
|
|
395
|
+
getData: () => "",
|
|
396
|
+
setData: () => {},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// TextEncoder fix for jsdom
|
|
403
|
+
class ESBuildAndJSDOMCompatibleTextEncoder extends TextEncoder {
|
|
404
|
+
encode(input: string) {
|
|
405
|
+
if (typeof input !== "string") throw new TypeError("`input` must be a string");
|
|
406
|
+
const decodedURI = decodeURIComponent(encodeURIComponent(input));
|
|
407
|
+
const arr = new Uint8Array(decodedURI.length);
|
|
408
|
+
const chars = decodedURI.split("");
|
|
409
|
+
for (let i = 0; i < chars.length; i++) {
|
|
410
|
+
arr[i] = (decodedURI[i] || "").charCodeAt(0);
|
|
411
|
+
}
|
|
412
|
+
return arr;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
Object.defineProperty(global, "TextEncoder", {
|
|
416
|
+
value: ESBuildAndJSDOMCompatibleTextEncoder,
|
|
417
|
+
writable: true,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Mock @multiplatform.one/theme
|
|
421
|
+
vi.mock("@multiplatform.one/theme", () => {
|
|
422
|
+
const emptyState = {};
|
|
423
|
+
const stateRecipe = {
|
|
424
|
+
hoverKnobProps: undefined,
|
|
425
|
+
pressKnobProps: undefined,
|
|
426
|
+
focusKnobProps: undefined,
|
|
427
|
+
focusVisibleKnobProps: undefined,
|
|
428
|
+
};
|
|
429
|
+
const knobProps = {
|
|
430
|
+
borderRadius: { borderRadius: "$4", borderWidth: 1 },
|
|
431
|
+
borderRadiusOuter: { borderRadius: "$5" },
|
|
432
|
+
elevation: undefined,
|
|
433
|
+
surface: { borderWidth: 1 },
|
|
434
|
+
inputSurface: { borderWidth: 1 },
|
|
435
|
+
panelPadding: { padding: "$4" },
|
|
436
|
+
gap: { gap: "$3" },
|
|
437
|
+
gapLg: { gap: "$4" },
|
|
438
|
+
sizeToken: "$4",
|
|
439
|
+
heading: { fontFamily: "$heading", fontWeight: "400" },
|
|
440
|
+
body: { fontFamily: "$body", fontWeight: "400" },
|
|
441
|
+
textWeight: { fontWeight: "400" },
|
|
442
|
+
transition: "quick",
|
|
443
|
+
outlined: false,
|
|
444
|
+
textAccent: "high",
|
|
445
|
+
space: "medium",
|
|
446
|
+
hoverStyle: emptyState,
|
|
447
|
+
pressStyle: emptyState,
|
|
448
|
+
focusStyle: emptyState,
|
|
449
|
+
focusVisibleStyle: emptyState,
|
|
450
|
+
};
|
|
451
|
+
return {
|
|
452
|
+
useResolvedKnobs: () => ({
|
|
453
|
+
knobProps,
|
|
454
|
+
control: { ...stateRecipe },
|
|
455
|
+
text: { ...stateRecipe },
|
|
456
|
+
elevation: { ...stateRecipe },
|
|
457
|
+
}),
|
|
458
|
+
};
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Mock 'one' package
|
|
462
|
+
vi.mock("one", () => ({
|
|
463
|
+
default: {},
|
|
464
|
+
SafeAreaView: ({ children }: { children: React.ReactNode }) => children,
|
|
465
|
+
Head: ({ children }: { children: React.ReactNode }) => children,
|
|
466
|
+
useRouter: () => ({
|
|
467
|
+
push: vi.fn(),
|
|
468
|
+
replace: vi.fn(),
|
|
469
|
+
back: vi.fn(),
|
|
470
|
+
}),
|
|
471
|
+
}));
|
|
472
|
+
|
|
473
|
+
// Mock @tamagui/animations-reanimated (used by @multiplatform.one/theme animations)
|
|
474
|
+
vi.mock("@tamagui/animations-reanimated", () => ({
|
|
475
|
+
createAnimations: (config: any) => ({
|
|
476
|
+
...config,
|
|
477
|
+
View: undefined,
|
|
478
|
+
Text: undefined,
|
|
479
|
+
isReactNative: false,
|
|
480
|
+
animations: config,
|
|
481
|
+
useAnimatedNumber: (initial: any) => ({ value: initial, setValue: () => {} }),
|
|
482
|
+
useAnimatedNumberReaction: () => {},
|
|
483
|
+
useAnimatedNumberStyle: () => ({}),
|
|
484
|
+
AnimatedView: ({ children }: any) => children,
|
|
485
|
+
AnimatedText: ({ children }: any) => children,
|
|
486
|
+
}),
|
|
487
|
+
}));
|
|
488
|
+
|
|
489
|
+
// Mock react-native-reanimated to prevent ESM resolution failures
|
|
490
|
+
// (publicGlobals missing .js extension in ESM import from index.js)
|
|
491
|
+
vi.mock("react-native-reanimated", () => ({
|
|
492
|
+
default: {
|
|
493
|
+
useSharedValue: (init: any) => ({ value: init }),
|
|
494
|
+
useAnimatedStyle: () => ({}),
|
|
495
|
+
withTiming: (val: any) => val,
|
|
496
|
+
withSpring: (val: any) => val,
|
|
497
|
+
withDecay: (val: any) => val,
|
|
498
|
+
withSequence: (...args: any[]) => args[args.length - 1],
|
|
499
|
+
withDelay: (_: any, val: any) => val,
|
|
500
|
+
createAnimatedComponent: (component: any) => component,
|
|
501
|
+
useDerivedValue: (fn: () => any) => ({ value: fn() }),
|
|
502
|
+
useAnimatedRef: () => ({ current: null }),
|
|
503
|
+
runOnJS: (fn: any) => fn,
|
|
504
|
+
runOnUI: (fn: any) => fn,
|
|
505
|
+
Extrapolation: { CLAMP: "clamp" },
|
|
506
|
+
Layout: { duration: () => ({}) },
|
|
507
|
+
FadeIn: { duration: () => ({}) },
|
|
508
|
+
FadeOut: { duration: () => ({}) },
|
|
509
|
+
Easing: {
|
|
510
|
+
linear: (v: any) => v,
|
|
511
|
+
ease: (v: any) => v,
|
|
512
|
+
bezier: () => (v: any) => v,
|
|
513
|
+
},
|
|
514
|
+
measure: () => null,
|
|
515
|
+
scrollTo: () => {},
|
|
516
|
+
makeMutable: (init: any) => ({ value: init }),
|
|
517
|
+
},
|
|
518
|
+
useSharedValue: (init: any) => ({ value: init }),
|
|
519
|
+
useAnimatedStyle: () => ({}),
|
|
520
|
+
withTiming: (val: any) => val,
|
|
521
|
+
withSpring: (val: any) => val,
|
|
522
|
+
createAnimatedComponent: (component: any) => component,
|
|
523
|
+
runOnJS: (fn: any) => fn,
|
|
524
|
+
runOnUI: (fn: any) => fn,
|
|
525
|
+
useDerivedValue: (fn: () => any) => ({ value: fn() }),
|
|
526
|
+
useAnimatedRef: () => ({ current: null }),
|
|
527
|
+
Easing: {
|
|
528
|
+
linear: (v: any) => v,
|
|
529
|
+
ease: (v: any) => v,
|
|
530
|
+
bezier: () => (v: any) => v,
|
|
531
|
+
},
|
|
532
|
+
FadeIn: { duration: () => ({}) },
|
|
533
|
+
FadeOut: { duration: () => ({}) },
|
|
534
|
+
Layout: { duration: () => ({}) },
|
|
535
|
+
}));
|
|
536
|
+
|
|
537
|
+
// NOTE: @phosphor-icons/react is NOT mocked here. Do NOT add vi.mock() for it
|
|
538
|
+
// in this setup file — doing so triggers vitest to resolve the full barrel
|
|
539
|
+
// (3024+ icon files) before the mock factory can replace it, causing an
|
|
540
|
+
// infinite hang. The real icons work fine in happy-dom (they're just SVG
|
|
541
|
+
// components), so no mock is needed.
|
|
542
|
+
|
|
543
|
+
// Mock @react-navigation/native
|
|
544
|
+
vi.mock("@react-navigation/native", () => ({
|
|
545
|
+
NavigationContainer: ({ children }: { children: React.ReactNode }) => children,
|
|
546
|
+
useNavigation: () => ({}),
|
|
547
|
+
useRoute: () => ({}),
|
|
548
|
+
useFocusEffect: () => {},
|
|
549
|
+
}));
|
|
550
|
+
|
|
551
|
+
// Cleanup after each test
|
|
552
|
+
// Fix pointer-events: none that Tamagui sets on <html>, which causes
|
|
553
|
+
// @testing-library/user-event to refuse pointer interactions.
|
|
554
|
+
if (typeof document !== "undefined") {
|
|
555
|
+
const style = document.createElement("style");
|
|
556
|
+
style.textContent = "html { pointer-events: auto !important; }";
|
|
557
|
+
document.head.appendChild(style);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const ignoredTestWarnings = [
|
|
561
|
+
"React does not recognize the `scaleIcon` prop on a DOM element",
|
|
562
|
+
"React does not recognize the `pressTheme` prop on a DOM element",
|
|
563
|
+
"React does not recognize the `marginLeft` prop on a DOM element",
|
|
564
|
+
"Received `true` for a non-boolean attribute `editable`",
|
|
565
|
+
"non-boolean attribute `editable`",
|
|
566
|
+
"Received `true` for a non-boolean attribute `elevate`",
|
|
567
|
+
"Received `true` for a non-boolean attribute `bordered`",
|
|
568
|
+
"react-i18next:: useTranslation: You will need to pass in an i18next instance",
|
|
569
|
+
];
|
|
570
|
+
|
|
571
|
+
const originalConsoleWarn = console.warn.bind(console);
|
|
572
|
+
const originalConsoleError = console.error.bind(console);
|
|
573
|
+
const shouldIgnoreTestWarning = (args: unknown[]) => {
|
|
574
|
+
const message = args
|
|
575
|
+
.map((arg) => {
|
|
576
|
+
if (typeof arg === "string") return arg;
|
|
577
|
+
if (arg instanceof Error) return arg.message;
|
|
578
|
+
return String(arg);
|
|
579
|
+
})
|
|
580
|
+
.join(" ");
|
|
581
|
+
if (ignoredTestWarnings.some((pattern) => message.includes(pattern))) {
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// React often logs warnings with printf placeholders:
|
|
586
|
+
// "Received `%s` for a non-boolean attribute `%s`." + substitution args.
|
|
587
|
+
const firstArg = typeof args[0] === "string" ? args[0] : "";
|
|
588
|
+
const hasNonBooleanTemplate = firstArg.includes("non-boolean attribute");
|
|
589
|
+
if (hasNonBooleanTemplate) {
|
|
590
|
+
const tokens = args.map((arg) => String(arg));
|
|
591
|
+
if (tokens.includes("editable") || tokens.includes("elevate") || tokens.includes("bordered")) {
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return false;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
let consoleWarnSpy: ReturnType<typeof vi.spyOn> | null = null;
|
|
600
|
+
let consoleErrorSpy: ReturnType<typeof vi.spyOn> | null = null;
|
|
601
|
+
|
|
602
|
+
beforeEach(() => {
|
|
603
|
+
Object.defineProperty(globalThis, "IntersectionObserver", {
|
|
604
|
+
value: MockIntersectionObserver,
|
|
605
|
+
writable: true,
|
|
606
|
+
configurable: true,
|
|
607
|
+
});
|
|
608
|
+
if (typeof window !== "undefined") {
|
|
609
|
+
Object.defineProperty(window, "IntersectionObserver", {
|
|
610
|
+
value: MockIntersectionObserver,
|
|
611
|
+
writable: true,
|
|
612
|
+
configurable: true,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
if (typeof global !== "undefined") {
|
|
616
|
+
Object.defineProperty(global, "IntersectionObserver", {
|
|
617
|
+
value: MockIntersectionObserver,
|
|
618
|
+
writable: true,
|
|
619
|
+
configurable: true,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation((...args: unknown[]) => {
|
|
623
|
+
if (shouldIgnoreTestWarning(args)) return;
|
|
624
|
+
originalConsoleWarn(...args);
|
|
625
|
+
});
|
|
626
|
+
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation((...args: unknown[]) => {
|
|
627
|
+
if (shouldIgnoreTestWarning(args)) return;
|
|
628
|
+
originalConsoleError(...args);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
afterEach(() => {
|
|
633
|
+
Object.defineProperty(globalThis, "IntersectionObserver", {
|
|
634
|
+
value: MockIntersectionObserver,
|
|
635
|
+
writable: true,
|
|
636
|
+
configurable: true,
|
|
637
|
+
});
|
|
638
|
+
if (typeof window !== "undefined") {
|
|
639
|
+
Object.defineProperty(window, "IntersectionObserver", {
|
|
640
|
+
value: MockIntersectionObserver,
|
|
641
|
+
writable: true,
|
|
642
|
+
configurable: true,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
consoleWarnSpy?.mockRestore();
|
|
646
|
+
consoleErrorSpy?.mockRestore();
|
|
647
|
+
consoleWarnSpy = null;
|
|
648
|
+
consoleErrorSpy = null;
|
|
649
|
+
cleanup();
|
|
650
|
+
});
|