expo-tvos-search 1.2.3 → 1.3.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/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "expo-tvos-search",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Native tvOS search view using SwiftUI .searchable modifier for Expo/React Native",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
7
+ "react-native": "src/index.tsx",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./build/index.d.ts",
11
+ "react-native": "./src/index.tsx",
12
+ "default": "./build/index.js"
13
+ }
14
+ },
7
15
  "repository": {
8
16
  "type": "git",
9
17
  "url": "git+https://github.com/keiver/expo-tvos-search.git"
@@ -104,7 +104,7 @@ describe('TvosSearchViewProps defaults', () => {
104
104
  // The actual defaults are applied in Swift (ExpoTvosSearchView.swift)
105
105
  const expectedDefaults = {
106
106
  columns: 5,
107
- placeholder: 'Search...',
107
+ placeholder: 'Search movies and videos...', // Matches Swift default
108
108
  isLoading: false,
109
109
  showTitle: false,
110
110
  showSubtitle: false,
@@ -113,6 +113,7 @@ describe('TvosSearchViewProps defaults', () => {
113
113
  showTitleOverlay: true,
114
114
  enableMarquee: true,
115
115
  marqueeDelay: 1.5,
116
+ overlayTitleSize: 20,
116
117
  };
117
118
 
118
119
  // Verify default documentation matches Swift implementation
@@ -120,5 +121,459 @@ describe('TvosSearchViewProps defaults', () => {
120
121
  expect(expectedDefaults.showTitleOverlay).toBe(true);
121
122
  expect(expectedDefaults.enableMarquee).toBe(true);
122
123
  expect(expectedDefaults.marqueeDelay).toBe(1.5);
124
+ expect(expectedDefaults.overlayTitleSize).toBe(20);
125
+ });
126
+ });
127
+
128
+ describe('TvosSearchViewProps overlayTitleSize', () => {
129
+ beforeEach(() => {
130
+ jest.resetModules();
131
+ mockTvOSPlatform();
132
+ mockNativeModuleAvailable();
133
+ });
134
+
135
+ it('accepts overlayTitleSize as a number', () => {
136
+ const { TvosSearchView } = require('../index');
137
+
138
+ // Should not throw when overlayTitleSize is provided
139
+ expect(() => {
140
+ TvosSearchView({
141
+ results: [],
142
+ onSearch: jest.fn(),
143
+ onSelectItem: jest.fn(),
144
+ overlayTitleSize: 18,
145
+ });
146
+ }).not.toThrow();
147
+ });
148
+
149
+ it('accepts overlayTitleSize with various values', () => {
150
+ const { TvosSearchView } = require('../index');
151
+
152
+ const testCases = [12, 18, 20, 24, 32];
153
+
154
+ testCases.forEach((size) => {
155
+ expect(() => {
156
+ TvosSearchView({
157
+ results: [],
158
+ onSearch: jest.fn(),
159
+ onSelectItem: jest.fn(),
160
+ overlayTitleSize: size,
161
+ });
162
+ }).not.toThrow();
163
+ });
164
+ });
165
+
166
+ it('works without overlayTitleSize (uses default)', () => {
167
+ const { TvosSearchView } = require('../index');
168
+
169
+ // Should not throw when overlayTitleSize is omitted
170
+ expect(() => {
171
+ TvosSearchView({
172
+ results: [],
173
+ onSearch: jest.fn(),
174
+ onSelectItem: jest.fn(),
175
+ });
176
+ }).not.toThrow();
177
+ });
178
+ });
179
+
180
+ describe('Module initialization error handling', () => {
181
+ let consoleWarnSpy: jest.SpyInstance;
182
+ let consoleErrorSpy: jest.SpyInstance;
183
+
184
+ beforeEach(() => {
185
+ jest.resetModules();
186
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
187
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
188
+ });
189
+
190
+ afterEach(() => {
191
+ consoleWarnSpy.mockRestore();
192
+ consoleErrorSpy.mockRestore();
193
+ jest.resetModules();
194
+ jest.dontMock('expo-modules-core');
195
+ jest.dontMock('react-native');
196
+ });
197
+
198
+ it('handles requireNativeViewManager not being a function', () => {
199
+ mockTvOSPlatform();
200
+
201
+ // Mock requireNativeViewManager as a string instead of function
202
+ jest.doMock('expo-modules-core', () => ({
203
+ requireNativeViewManager: 'not-a-function',
204
+ }));
205
+
206
+ const { isNativeSearchAvailable } = require('../index');
207
+
208
+ expect(isNativeSearchAvailable()).toBe(false);
209
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
210
+ expect.stringContaining('requireNativeViewManager is not a function')
211
+ );
212
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
213
+ expect.stringContaining('incompatible expo-modules-core version')
214
+ );
215
+ });
216
+
217
+ it('handles expo-modules-core missing error', () => {
218
+ mockTvOSPlatform();
219
+
220
+ // Mock expo-modules-core to throw error
221
+ jest.doMock('expo-modules-core', () => {
222
+ throw new Error('Cannot find module expo-modules-core');
223
+ });
224
+
225
+ const { isNativeSearchAvailable } = require('../index');
226
+
227
+ expect(isNativeSearchAvailable()).toBe(false);
228
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
229
+ expect.stringContaining('Failed to load expo-modules-core')
230
+ );
231
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
232
+ expect.stringContaining('npm install expo-modules-core')
233
+ );
234
+ });
235
+
236
+ it('handles ExpoTvosSearch module not found error', () => {
237
+ mockTvOSPlatform();
238
+
239
+ // Mock requireNativeViewManager to throw module not found error
240
+ jest.doMock('expo-modules-core', () => ({
241
+ requireNativeViewManager: jest.fn(() => {
242
+ throw new Error('Native module ExpoTvosSearch not found');
243
+ }),
244
+ }));
245
+
246
+ const { isNativeSearchAvailable } = require('../index');
247
+
248
+ expect(isNativeSearchAvailable()).toBe(false);
249
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
250
+ expect.stringContaining('Native module ExpoTvosSearch not found')
251
+ );
252
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
253
+ expect.stringContaining("haven't run 'expo prebuild'")
254
+ );
255
+ });
256
+
257
+ it('handles unexpected error', () => {
258
+ mockTvOSPlatform();
259
+
260
+ // Mock requireNativeViewManager to throw unexpected error
261
+ jest.doMock('expo-modules-core', () => ({
262
+ requireNativeViewManager: jest.fn(() => {
263
+ throw new Error('Some unexpected error');
264
+ }),
265
+ }));
266
+
267
+ const { isNativeSearchAvailable } = require('../index');
268
+
269
+ expect(isNativeSearchAvailable()).toBe(false);
270
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
271
+ expect.stringContaining('Unexpected error loading native module')
272
+ );
273
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
274
+ expect.stringContaining('Some unexpected error')
275
+ );
276
+ });
277
+
278
+ it('logs full error details in development mode for unexpected errors', () => {
279
+ // Set __DEV__ to true
280
+ (global as any).__DEV__ = true;
281
+
282
+ mockTvOSPlatform();
283
+
284
+ const testError = new Error('Unexpected development error');
285
+ jest.doMock('expo-modules-core', () => ({
286
+ requireNativeViewManager: jest.fn(() => {
287
+ throw testError;
288
+ }),
289
+ }));
290
+
291
+ const { isNativeSearchAvailable } = require('../index');
292
+
293
+ expect(isNativeSearchAvailable()).toBe(false);
294
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
295
+ expect.stringContaining('Unexpected error loading native module')
296
+ );
297
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
298
+ '[expo-tvos-search] Full error details:',
299
+ testError
300
+ );
301
+
302
+ // Clean up __DEV__
303
+ delete (global as any).__DEV__;
304
+ });
305
+
306
+ it('does not log full error details when __DEV__ is false', () => {
307
+ // Set __DEV__ to false
308
+ (global as any).__DEV__ = false;
309
+
310
+ mockTvOSPlatform();
311
+
312
+ jest.doMock('expo-modules-core', () => ({
313
+ requireNativeViewManager: jest.fn(() => {
314
+ throw new Error('Production error');
315
+ }),
316
+ }));
317
+
318
+ const { isNativeSearchAvailable } = require('../index');
319
+
320
+ expect(isNativeSearchAvailable()).toBe(false);
321
+ expect(consoleWarnSpy).toHaveBeenCalled();
322
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
323
+
324
+ // Clean up __DEV__
325
+ delete (global as any).__DEV__;
326
+ });
327
+
328
+ it('does not log full error details when __DEV__ is undefined', () => {
329
+ // Ensure __DEV__ is undefined
330
+ delete (global as any).__DEV__;
331
+
332
+ mockTvOSPlatform();
333
+
334
+ jest.doMock('expo-modules-core', () => ({
335
+ requireNativeViewManager: jest.fn(() => {
336
+ throw new Error('No DEV error');
337
+ }),
338
+ }));
339
+
340
+ const { isNativeSearchAvailable } = require('../index');
341
+
342
+ expect(isNativeSearchAvailable()).toBe(false);
343
+ expect(consoleWarnSpy).toHaveBeenCalled();
344
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
345
+ });
346
+ });
347
+
348
+ describe('TvosSearchView development logging', () => {
349
+ let consoleWarnSpy: jest.SpyInstance;
350
+ let consoleInfoSpy: jest.SpyInstance;
351
+
352
+ beforeEach(() => {
353
+ jest.resetModules();
354
+ jest.dontMock('expo-modules-core');
355
+ jest.dontMock('react-native');
356
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
357
+ consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
358
+ });
359
+
360
+ afterEach(() => {
361
+ consoleWarnSpy.mockRestore();
362
+ consoleInfoSpy.mockRestore();
363
+ delete (global as any).__DEV__;
364
+ });
365
+
366
+ it('warns in development when on tvOS but native module unavailable', () => {
367
+ // Set __DEV__ to true
368
+ (global as any).__DEV__ = true;
369
+
370
+ mockTvOSPlatform();
371
+ mockNativeModuleUnavailable();
372
+
373
+ const { TvosSearchView } = require('../index');
374
+ const result = TvosSearchView({
375
+ results: [],
376
+ onSearch: jest.fn(),
377
+ onSelectItem: jest.fn(),
378
+ });
379
+
380
+ expect(result).toBeNull();
381
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
382
+ expect.stringContaining('TvosSearchView is rendering null on tvOS')
383
+ );
384
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
385
+ expect.stringContaining("native module wasn't built properly")
386
+ );
387
+ });
388
+
389
+ it('logs info in development when on non-tvOS platform', () => {
390
+ // Set __DEV__ to true
391
+ (global as any).__DEV__ = true;
392
+
393
+ mockWebPlatform();
394
+ mockNativeModuleUnavailable();
395
+
396
+ const { TvosSearchView } = require('../index');
397
+ const result = TvosSearchView({
398
+ results: [],
399
+ onSearch: jest.fn(),
400
+ onSelectItem: jest.fn(),
401
+ });
402
+
403
+ expect(result).toBeNull();
404
+ expect(consoleInfoSpy).toHaveBeenCalledWith(
405
+ expect.stringContaining('TvosSearchView is not available on web')
406
+ );
407
+ expect(consoleInfoSpy).toHaveBeenCalledWith(
408
+ expect.stringContaining('Use isNativeSearchAvailable()')
409
+ );
410
+ });
411
+
412
+ it('does not log when __DEV__ is false', () => {
413
+ // Set __DEV__ to false
414
+ (global as any).__DEV__ = false;
415
+
416
+ mockTvOSPlatform();
417
+ mockNativeModuleUnavailable();
418
+
419
+ const { TvosSearchView } = require('../index');
420
+ const result = TvosSearchView({
421
+ results: [],
422
+ onSearch: jest.fn(),
423
+ onSelectItem: jest.fn(),
424
+ });
425
+
426
+ expect(result).toBeNull();
427
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
428
+ expect(consoleInfoSpy).not.toHaveBeenCalled();
429
+ });
430
+
431
+ it('does not log when __DEV__ is undefined', () => {
432
+ // Ensure __DEV__ is undefined (afterEach will clean up anyway)
433
+ delete (global as any).__DEV__;
434
+
435
+ mockWebPlatform();
436
+ mockNativeModuleUnavailable();
437
+
438
+ const { TvosSearchView } = require('../index');
439
+ const result = TvosSearchView({
440
+ results: [],
441
+ onSearch: jest.fn(),
442
+ onSelectItem: jest.fn(),
443
+ });
444
+
445
+ expect(result).toBeNull();
446
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
447
+ expect(consoleInfoSpy).not.toHaveBeenCalled();
448
+ });
449
+
450
+ it('logs info for iOS (non-TV) platform in development', () => {
451
+ // Set __DEV__ to true
452
+ (global as any).__DEV__ = true;
453
+
454
+ // Use the helper functions instead of doMock
455
+ globalThis.__mockPlatformOS = 'ios';
456
+ globalThis.__mockPlatformIsTV = false;
457
+ globalThis.__mockNativeViewAvailable = false;
458
+
459
+ const { TvosSearchView } = require('../index');
460
+ const result = TvosSearchView({
461
+ results: [],
462
+ onSearch: jest.fn(),
463
+ onSelectItem: jest.fn(),
464
+ });
465
+
466
+ expect(result).toBeNull();
467
+ expect(consoleInfoSpy).toHaveBeenCalledWith(
468
+ expect.stringContaining('TvosSearchView is not available on ios')
469
+ );
470
+ });
471
+ });
472
+
473
+ describe('TvosSearchView with native module available', () => {
474
+ beforeEach(() => {
475
+ jest.resetModules();
476
+ jest.dontMock('expo-modules-core');
477
+ jest.dontMock('react-native');
478
+ mockTvOSPlatform();
479
+ mockNativeModuleAvailable();
480
+ });
481
+
482
+ it('renders when NativeView is available', () => {
483
+ const { TvosSearchView } = require('../index');
484
+
485
+ const result = TvosSearchView({
486
+ results: [],
487
+ onSearch: jest.fn(),
488
+ onSelectItem: jest.fn(),
489
+ });
490
+
491
+ // Should render JSX element, not null
492
+ expect(result).not.toBeNull();
493
+ expect(result).toBeTruthy();
494
+ });
495
+
496
+ it('forwards props to NativeView', () => {
497
+ const { TvosSearchView } = require('../index');
498
+
499
+ const mockOnSearch = jest.fn();
500
+ const mockOnSelectItem = jest.fn();
501
+ const mockOnError = jest.fn();
502
+ const mockResults = [
503
+ { id: '1', title: 'Test 1', subtitle: 'Subtitle 1' },
504
+ { id: '2', title: 'Test 2' },
505
+ ];
506
+
507
+ const result = TvosSearchView({
508
+ results: mockResults,
509
+ columns: 3,
510
+ placeholder: 'Search...',
511
+ isLoading: true,
512
+ showTitle: true,
513
+ showSubtitle: true,
514
+ showFocusBorder: true,
515
+ topInset: 100,
516
+ textColor: '#FFFFFF',
517
+ accentColor: '#FF0000',
518
+ cardWidth: 300,
519
+ cardHeight: 400,
520
+ onSearch: mockOnSearch,
521
+ onSelectItem: mockOnSelectItem,
522
+ onError: mockOnError,
523
+ });
524
+
525
+ // Component should render
526
+ expect(result).not.toBeNull();
527
+ });
528
+
529
+ it('renders with minimal required props', () => {
530
+ const { TvosSearchView } = require('../index');
531
+
532
+ const result = TvosSearchView({
533
+ results: [],
534
+ onSearch: jest.fn(),
535
+ onSelectItem: jest.fn(),
536
+ });
537
+
538
+ expect(result).not.toBeNull();
539
+ });
540
+
541
+ it('renders with all optional props', () => {
542
+ const { TvosSearchView } = require('../index');
543
+
544
+ const mockOnValidationWarning = jest.fn();
545
+
546
+ const result = TvosSearchView({
547
+ results: [{ id: 'test', title: 'Test', subtitle: 'Sub', imageUrl: 'http://example.com/img.jpg' }],
548
+ columns: 5,
549
+ placeholder: 'Custom placeholder',
550
+ isLoading: false,
551
+ showTitle: true,
552
+ showSubtitle: true,
553
+ showFocusBorder: true,
554
+ topInset: 140,
555
+ showTitleOverlay: false,
556
+ enableMarquee: false,
557
+ marqueeDelay: 2.0,
558
+ emptyStateText: 'Nothing here',
559
+ searchingText: 'Looking...',
560
+ noResultsText: 'Not found',
561
+ noResultsHintText: 'Try again',
562
+ textColor: '#E5E5E5',
563
+ accentColor: '#FFC312',
564
+ cardWidth: 280,
565
+ cardHeight: 420,
566
+ imageContentMode: 'fit',
567
+ cardMargin: 40,
568
+ cardPadding: 16,
569
+ overlayTitleSize: 20,
570
+ onSearch: jest.fn(),
571
+ onSelectItem: jest.fn(),
572
+ onError: jest.fn(),
573
+ onValidationWarning: mockOnValidationWarning,
574
+ style: { flex: 1 },
575
+ });
576
+
577
+ expect(result).not.toBeNull();
123
578
  });
124
579
  });