@@ -2,13 +2,18 @@ import { renderHook, waitFor } from "@testing-library/react";
22import hash from "stable-hash" ;
33import { useApplyLens } from "./useApplyLens" ;
44import { useInternalCameraKit } from "./CameraKitProvider" ;
5+ import { metricsReporter } from "./internal/metrics" ;
56
67jest . mock ( "stable-hash" ) ;
78jest . mock ( "@snap/camera-kit" , ( ) => ( { } ) ) ;
89jest . mock ( "./CameraKitProvider" ) ;
10+ jest . mock ( "./internal/metrics" , ( ) => ( {
11+ metricsReporter : { reportCount : jest . fn ( ) } ,
12+ } ) ) ;
913
1014const mockUseInternalCameraKit = useInternalCameraKit as jest . MockedFunction < typeof useInternalCameraKit > ;
1115const mockHash = hash as jest . MockedFunction < typeof hash > ;
16+ const mockReportCount = metricsReporter . reportCount as jest . Mock ;
1217
1318describe ( "useApplyLens" , ( ) => {
1419 let mockApplyLens : jest . Mock ;
@@ -17,6 +22,7 @@ describe("useApplyLens", () => {
1722 let mockLogger : any ;
1823 let mockCameraKit : any ;
1924 let mockSession : any ;
25+ let mockReinitialize : jest . Mock ;
2026
2127 beforeEach ( ( ) => {
2228 jest . clearAllMocks ( ) ;
@@ -31,16 +37,19 @@ describe("useApplyLens", () => {
3137 mockApplyLens = jest . fn ( ) . mockResolvedValue ( true ) ;
3238 mockRemoveLens = jest . fn ( ) . mockResolvedValue ( true ) ;
3339 mockGetLogger = jest . fn ( ) . mockReturnValue ( mockLogger ) ;
40+ mockReinitialize = jest . fn ( ) ;
3441
3542 mockCameraKit = { id : "mock-kit" } ;
3643 mockSession = { id : "mock-session" } ;
3744
3845 mockUseInternalCameraKit . mockReturnValue ( {
3946 cameraKit : mockCameraKit ,
4047 sdkStatus : "ready" ,
48+ sdkError : undefined ,
4149 currentSession : mockSession ,
4250 applyLens : mockApplyLens ,
4351 removeLens : mockRemoveLens ,
52+ reinitialize : mockReinitialize ,
4453 getLogger : mockGetLogger ,
4554 } as any ) ;
4655
@@ -401,4 +410,115 @@ describe("useApplyLens", () => {
401410 } ) ;
402411 } ) ;
403412 } ) ;
413+
414+ describe ( "Auto-recovery on LensAbortError" , ( ) => {
415+ const abortError = Object . assign ( new Error ( "aborted" ) , { name : "LensAbortError" } ) ;
416+
417+ const errorContext = ( sdkError : Error ) =>
418+ ( {
419+ cameraKit : null ,
420+ sdkStatus : "error" ,
421+ sdkError,
422+ currentSession : null ,
423+ applyLens : mockApplyLens ,
424+ removeLens : mockRemoveLens ,
425+ reinitialize : mockReinitialize ,
426+ getLogger : mockGetLogger ,
427+ } ) as any ;
428+
429+ it ( "reinitializes when lensId changes while SDK is in a LensAbortError" , ( ) => {
430+ mockUseInternalCameraKit . mockReturnValue ( errorContext ( abortError ) ) ;
431+
432+ const { rerender } = renderHook ( ( { lensId } ) => useApplyLens ( lensId , "group-1" ) , {
433+ initialProps : { lensId : "lens-1" } ,
434+ } ) ;
435+
436+ // Must not fire on mount.
437+ expect ( mockReinitialize ) . not . toHaveBeenCalled ( ) ;
438+
439+ rerender ( { lensId : "lens-2" } ) ;
440+
441+ expect ( mockReinitialize ) . toHaveBeenCalledTimes ( 1 ) ;
442+ // Recovery only un-wedges the SDK; it does not apply the lens itself.
443+ expect ( mockApplyLens ) . not . toHaveBeenCalled ( ) ;
444+ } ) ;
445+
446+ it ( "reinitializes when launch data changes while in a LensAbortError" , ( ) => {
447+ mockHash . mockImplementation ( ( obj ) => JSON . stringify ( obj ) ) ;
448+ mockUseInternalCameraKit . mockReturnValue ( errorContext ( abortError ) ) ;
449+
450+ const { rerender } = renderHook ( ( { launchData } ) => useApplyLens ( "lens-1" , "group-1" , launchData ) , {
451+ initialProps : { launchData : { launchParams : { hint : "face" } } } ,
452+ } ) ;
453+
454+ rerender ( { launchData : { launchParams : { hint : "hand" } } } ) ;
455+
456+ expect ( mockReinitialize ) . toHaveBeenCalledTimes ( 1 ) ;
457+ } ) ;
458+
459+ it ( "does NOT reinitialize for a bootstrap-failure error" , ( ) => {
460+ const bootError = Object . assign ( new Error ( "boot failed" ) , { name : "BootstrapError" } ) ;
461+ mockUseInternalCameraKit . mockReturnValue ( errorContext ( bootError ) ) ;
462+
463+ const { rerender } = renderHook ( ( { lensId } ) => useApplyLens ( lensId , "group-1" ) , {
464+ initialProps : { lensId : "lens-1" } ,
465+ } ) ;
466+
467+ rerender ( { lensId : "lens-2" } ) ;
468+
469+ expect ( mockReinitialize ) . not . toHaveBeenCalled ( ) ;
470+ } ) ;
471+
472+ it ( "does NOT reinitialize when no target lens is set" , ( ) => {
473+ mockUseInternalCameraKit . mockReturnValue ( errorContext ( abortError ) ) ;
474+
475+ const { rerender } = renderHook ( ( { groupId } ) => useApplyLens ( undefined , groupId ) , {
476+ initialProps : { groupId : "group-1" } ,
477+ } ) ;
478+
479+ rerender ( { groupId : "group-2" } ) ;
480+
481+ expect ( mockReinitialize ) . not . toHaveBeenCalled ( ) ;
482+ } ) ;
483+
484+ it ( "does NOT reinitialize when SDK is ready (no regression)" , async ( ) => {
485+ const { rerender } = renderHook ( ( { lensId } ) => useApplyLens ( lensId , "group-1" ) , {
486+ initialProps : { lensId : "lens-1" } ,
487+ } ) ;
488+
489+ rerender ( { lensId : "lens-2" } ) ;
490+
491+ await waitFor ( ( ) => {
492+ expect ( mockApplyLens ) . toHaveBeenCalledWith ( "lens-2" , "group-1" , undefined , undefined ) ;
493+ } ) ;
494+ expect ( mockReinitialize ) . not . toHaveBeenCalled ( ) ;
495+ } ) ;
496+
497+ it ( "reinitializes only once for a lens that keeps aborting under the same id" , ( ) => {
498+ mockUseInternalCameraKit . mockReturnValue ( errorContext ( abortError ) ) ;
499+
500+ const { rerender } = renderHook ( ( { lensId } ) => useApplyLens ( lensId , "group-1" ) , {
501+ initialProps : { lensId : "lens-1" } ,
502+ } ) ;
503+
504+ rerender ( { lensId : "lens-2" } ) ;
505+ expect ( mockReinitialize ) . toHaveBeenCalledTimes ( 1 ) ;
506+
507+ // The new lens re-aborts; the id has not changed, so we must not reinit again.
508+ rerender ( { lensId : "lens-2" } ) ;
509+ expect ( mockReinitialize ) . toHaveBeenCalledTimes ( 1 ) ;
510+ } ) ;
511+
512+ it ( "emits the auto_reinit_on_lens_change metric when it fires" , ( ) => {
513+ mockUseInternalCameraKit . mockReturnValue ( errorContext ( abortError ) ) ;
514+
515+ const { rerender } = renderHook ( ( { lensId } ) => useApplyLens ( lensId , "group-1" ) , {
516+ initialProps : { lensId : "lens-1" } ,
517+ } ) ;
518+
519+ rerender ( { lensId : "lens-2" } ) ;
520+
521+ expect ( mockReportCount ) . toHaveBeenCalledWith ( "auto_reinit_on_lens_change" ) ;
522+ } ) ;
523+ } ) ;
404524} ) ;
0 commit comments