11import axios from 'axios'
22import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
33
4+ import type { IWidget } from '@/lib/litegraph/src/litegraph'
5+ import { LGraphNode } from '@/lib/litegraph/src/litegraph'
46import { useRemoteWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget'
57import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
68
7- vi . mock ( 'axios' , ( ) => {
9+ const createMockNode = ( overrides : Partial < LGraphNode > = { } ) : LGraphNode => {
10+ const node = new LGraphNode ( 'TestNode' )
11+ Object . assign ( node , overrides )
12+ return node
13+ }
14+
15+ const createMockWidget = ( overrides = { } ) : IWidget =>
16+ ( { ...overrides } ) as unknown as IWidget
17+
18+ const mockCloudAuth = vi . hoisted ( ( ) => ( {
19+ isCloud : false ,
20+ authHeader : null as { Authorization : string } | null
21+ } ) )
22+
23+ vi . mock ( 'axios' , async ( importOriginal ) => {
24+ const actual = await importOriginal < typeof import ( 'axios' ) > ( )
825 return {
926 default : {
27+ ...actual . default ,
1028 get : vi . fn ( )
1129 }
1230 }
1331} )
1432
15- vi . mock ( '@/i18n' , ( ) => ( {
16- i18n : {
17- global : {
18- t : vi . fn ( ( key ) => key )
19- }
33+ vi . mock ( '@/platform/distribution/types' , ( ) => ( {
34+ get isCloud ( ) {
35+ return mockCloudAuth . isCloud
2036 }
2137} ) )
2238
23- vi . mock ( '@/platform/settings/settingStore' , ( ) => ( {
24- useSettingStore : ( ) => ( {
25- settings : { }
26- } )
27- } ) )
28-
29- vi . mock ( '@/scripts/api' , ( ) => ( {
30- api : {
31- addEventListener : vi . fn ( ) ,
32- removeEventListener : vi . fn ( )
39+ vi . mock ( '@/stores/firebaseAuthStore' , async ( importOriginal ) => {
40+ const actual =
41+ await importOriginal < typeof import ( '@/stores/firebaseAuthStore' ) > ( )
42+ return {
43+ ...actual ,
44+ useFirebaseAuthStore : vi . fn ( ( ) => ( {
45+ getAuthHeader : vi . fn ( ( ) => Promise . resolve ( mockCloudAuth . authHeader ) )
46+ } ) )
3347 }
34- } ) )
48+ } )
3549
36- vi . mock ( '@/composables/functional/useChainCallback' , ( ) => ( {
37- useChainCallback : vi . fn ( ( original , ...callbacks ) => {
38- return function ( this : any , ...args : any [ ] ) {
39- original ?. apply ( this , args )
40- callbacks . forEach ( ( cb : any ) => cb . apply ( this , args ) )
41- }
42- } )
43- } ) )
50+ vi . mock ( '@/platform/settings/settingStore' , async ( importOriginal ) => {
51+ const actual =
52+ await importOriginal < typeof import ( '@/platform/settings/settingStore' ) > ( )
53+ return {
54+ ...actual ,
55+ useSettingStore : ( ) => ( {
56+ settings : { }
57+ } )
58+ }
59+ } )
4460
4561const FIRST_BACKOFF = 1000 // backoff is 1s on first retry
4662const DEFAULT_VALUE = 'Loading ...'
@@ -56,10 +72,8 @@ function createMockConfig(overrides = {}): RemoteWidgetConfig {
5672const createMockOptions = ( inputOverrides = { } ) => ( {
5773 remoteConfig : createMockConfig ( inputOverrides ) ,
5874 defaultValue : DEFAULT_VALUE ,
59- node : {
60- addWidget : vi . fn ( )
61- } as any ,
62- widget : { } as any
75+ node : createMockNode ( ) ,
76+ widget : createMockWidget ( )
6377} )
6478
6579function mockAxiosResponse ( data : unknown, status = 200 ) {
@@ -224,12 +238,19 @@ describe('useRemoteWidget', () => {
224238 const { hook } = await setupHookWithResponse ( mockData )
225239
226240 await getResolvedValue ( hook )
241+ expect ( hook . getCachedValue ( ) ) . toEqual ( mockData )
242+
227243 const refreshedData = [ 'data that user forced to be fetched' ]
228244 mockAxiosResponse ( refreshedData )
229245
230246 hook . refreshValue ( )
231- const data = await getResolvedValue ( hook )
232- expect ( data ) . toEqual ( refreshedData )
247+
248+ // Wait for cache to update with refreshed data
249+ await vi . waitFor ( ( ) => {
250+ expect ( hook . getCachedValue ( ) ) . toEqual ( refreshedData )
251+ } )
252+
253+ expect ( vi . mocked ( axios . get ) ) . toHaveBeenCalledTimes ( 2 )
233254 } )
234255
235256 it ( 'permanent widgets should still retry if request fails' , async ( ) => {
@@ -417,16 +438,25 @@ describe('useRemoteWidget', () => {
417438 } )
418439
419440 it ( 'should prevent duplicate in-flight requests' , async ( ) => {
420- const promise = Promise . resolve ( { data : [ 'non-duplicate' ] } )
421- vi . mocked ( axios . get ) . mockImplementationOnce ( ( ) => promise as any )
441+ const mockData = [ 'non-duplicate' ]
442+ mockAxiosResponse ( mockData )
422443
423444 const hook = useRemoteWidget ( createMockOptions ( ) )
424- const [ result1 , result2 ] = await Promise . all ( [
425- getResolvedValue ( hook ) ,
426- getResolvedValue ( hook )
427- ] )
428445
429- expect ( result1 ) . toBe ( result2 )
446+ // Start two concurrent getValue calls
447+ const promise1 = new Promise < void > ( ( resolve ) => {
448+ hook . getValue ( ( ) => resolve ( ) )
449+ } )
450+ const promise2 = new Promise < void > ( ( resolve ) => {
451+ hook . getValue ( ( ) => resolve ( ) )
452+ } )
453+
454+ // Wait for both e
455+ await Promise . all ( [ promise1 , promise2 ] )
456+
457+ // Both should see the same cached data
458+ expect ( hook . getCachedValue ( ) ) . toEqual ( mockData )
459+ // Only one axios call should have been made
430460 expect ( vi . mocked ( axios . get ) ) . toHaveBeenCalledTimes ( 1 )
431461 } )
432462 } )
@@ -518,6 +548,44 @@ describe('useRemoteWidget', () => {
518548 } )
519549 } )
520550
551+ describe ( 'cloud distribution authentication' , ( ) => {
552+ describe ( 'when distribution is cloud' , ( ) => {
553+ describe ( 'when authenticated' , ( ) => {
554+ it ( 'passes Firebase authentication token in request headers' , async ( ) => {
555+ const mockData = [ 'authenticated data' ]
556+ mockCloudAuth . authHeader = null
557+ mockCloudAuth . isCloud = true
558+ mockCloudAuth . authHeader = { Authorization : 'Bearer test-token' }
559+ mockAxiosResponse ( mockData )
560+
561+ const hook = useRemoteWidget ( createMockOptions ( ) )
562+ await getResolvedValue ( hook )
563+
564+ expect ( vi . mocked ( axios . get ) ) . toHaveBeenCalledWith (
565+ expect . any ( String ) ,
566+ expect . objectContaining ( {
567+ headers : { Authorization : 'Bearer test-token' }
568+ } )
569+ )
570+ } )
571+ } )
572+ } )
573+
574+ describe ( 'when distribution is not cloud' , ( ) => {
575+ it ( 'bypasses authentication for non-cloud environments' , async ( ) => {
576+ const mockData = [ 'non-cloud data' ]
577+ mockCloudAuth . isCloud = false
578+ mockAxiosResponse ( mockData )
579+
580+ const hook = useRemoteWidget ( createMockOptions ( ) )
581+ await getResolvedValue ( hook )
582+
583+ const axiosCall = vi . mocked ( axios . get ) . mock . calls [ 0 ] [ 1 ]
584+ expect ( axiosCall ) . not . toHaveProperty ( 'headers' )
585+ } )
586+ } )
587+ } )
588+
521589 describe ( 'auto-refresh on task completion' , ( ) => {
522590 it ( 'should add auto-refresh toggle widget' , ( ) => {
523591 const mockNode = {
@@ -550,6 +618,7 @@ describe('useRemoteWidget', () => {
550618
551619 it ( 'should register event listener when enabled' , async ( ) => {
552620 const { api } = await import ( '@/scripts/api' )
621+ const addEventListenerSpy = vi . spyOn ( api , 'addEventListener' )
553622
554623 const mockNode = {
555624 addWidget : vi . fn ( ) ,
@@ -567,7 +636,7 @@ describe('useRemoteWidget', () => {
567636 } )
568637
569638 // Event listener should be registered immediately
570- expect ( api . addEventListener ) . toHaveBeenCalledWith (
639+ expect ( addEventListenerSpy ) . toHaveBeenCalledWith (
571640 'execution_success' ,
572641 expect . any ( Function )
573642 )
@@ -577,8 +646,7 @@ describe('useRemoteWidget', () => {
577646 const { api } = await import ( '@/scripts/api' )
578647 let executionSuccessHandler : ( ( ) => void ) | undefined
579648
580- // Capture the event handler
581- vi . mocked ( api . addEventListener ) . mockImplementation ( ( event , handler ) => {
649+ vi . spyOn ( api , 'addEventListener' ) . mockImplementation ( ( event , handler ) => {
582650 if ( event === 'execution_success' ) {
583651 executionSuccessHandler = handler as ( ) => void
584652 }
@@ -616,8 +684,7 @@ describe('useRemoteWidget', () => {
616684 const { api } = await import ( '@/scripts/api' )
617685 let executionSuccessHandler : ( ( ) => void ) | undefined
618686
619- // Capture the event handler
620- vi . mocked ( api . addEventListener ) . mockImplementation ( ( event , handler ) => {
687+ vi . spyOn ( api , 'addEventListener' ) . mockImplementation ( ( event , handler ) => {
621688 if ( event === 'execution_success' ) {
622689 executionSuccessHandler = handler as ( ) => void
623690 }
@@ -650,13 +717,14 @@ describe('useRemoteWidget', () => {
650717 const { api } = await import ( '@/scripts/api' )
651718 let executionSuccessHandler : ( ( ) => void ) | undefined
652719
653- // Capture the event handler
654- vi . mocked ( api . addEventListener ) . mockImplementation ( ( event , handler ) => {
720+ vi . spyOn ( api , 'addEventListener' ) . mockImplementation ( ( event , handler ) => {
655721 if ( event === 'execution_success' ) {
656722 executionSuccessHandler = handler as ( ) => void
657723 }
658724 } )
659725
726+ const removeEventListenerSpy = vi . spyOn ( api , 'removeEventListener' )
727+
660728 const mockNode = {
661729 addWidget : vi . fn ( ) ,
662730 widgets : [ ] ,
@@ -676,7 +744,7 @@ describe('useRemoteWidget', () => {
676744 // Simulate node removal
677745 mockNode . onRemoved ?.( )
678746
679- expect ( api . removeEventListener ) . toHaveBeenCalledWith (
747+ expect ( removeEventListenerSpy ) . toHaveBeenCalledWith (
680748 'execution_success' ,
681749 executionSuccessHandler
682750 )
0 commit comments