@@ -91,6 +91,10 @@ describe("App", () => {
9191 } ) ;
9292
9393 afterEach ( ( ) => {
94+ Object . defineProperty ( document , "visibilityState" , {
95+ configurable : true ,
96+ value : "visible" ,
97+ } ) ;
9498 vi . unstubAllGlobals ( ) ;
9599 vi . useRealTimers ( ) ;
96100 } ) ;
@@ -389,7 +393,6 @@ describe("App", () => {
389393 await act ( async ( ) => {
390394 await Promise . resolve ( ) ;
391395 } ) ;
392-
393396 expect ( screen . getByText ( / a c c o u n t s - s y n c : 0 / ) ) . toBeInTheDocument ( ) ;
394397 expect ( mockedUpdateService . check ) . toHaveBeenCalledTimes ( 1 ) ;
395398
@@ -605,7 +608,6 @@ describe("App", () => {
605608 await act ( async ( ) => {
606609 await Promise . resolve ( ) ;
607610 } ) ;
608-
609611 expect ( screen . getByText ( / a c c o u n t s - s y n c : 0 / ) ) . toBeInTheDocument ( ) ;
610612 expect ( vi . mocked ( refreshDesktopTrayState ) ) . toHaveBeenCalledTimes ( 1 ) ;
611613
@@ -619,6 +621,69 @@ describe("App", () => {
619621 expect ( fetchMock ) . toHaveBeenCalledWith ( "http://127.0.0.1:6789/ai-router/api/settings/proxy/status" ) ;
620622 } ) ;
621623
624+ it ( "skips periodic status refresh while the main page is hidden" , async ( ) => {
625+ vi . useFakeTimers ( ) ;
626+ const fetchMock = vi . fn ( ( input : RequestInfo | URL ) => {
627+ const url = String ( input ) ;
628+ if ( url === "http://127.0.0.1:6789/ai-router/api/settings/proxy/status" ) {
629+ return Promise . resolve ( new Response ( JSON . stringify ( { enabled : false } ) , { status : 200 , headers : { "Content-Type" : "application/json" } } ) ) ;
630+ }
631+ if ( url === "http://127.0.0.1:6789/ai-router/api/settings/app" ) {
632+ return Promise . resolve (
633+ new Response (
634+ JSON . stringify ( {
635+ launch_at_login : false ,
636+ silent_start : false ,
637+ close_to_tray : true ,
638+ show_proxy_switch_on_home : true ,
639+ show_home_update_indicator : false ,
640+ status_refresh_interval_seconds : 5 ,
641+ proxy_host : "127.0.0.1" ,
642+ proxy_port : 6789 ,
643+ auto_failover_enabled : false ,
644+ auto_backup_interval_hours : 24 ,
645+ backup_retention_count : 10 ,
646+ audit_limit_message : 200 ,
647+ audit_limit_function_call : 100 ,
648+ audit_limit_function_call_output : 100 ,
649+ audit_limit_reasoning : 40 ,
650+ audit_limit_custom_tool_call : 100 ,
651+ audit_limit_custom_tool_call_output : 100 ,
652+ language : "zh-CN" ,
653+ theme_mode : "system" ,
654+ } ) ,
655+ { status : 200 , headers : { "Content-Type" : "application/json" } } ,
656+ ) ,
657+ ) ;
658+ }
659+ return Promise . resolve ( new Response ( null , { status : 404 } ) ) ;
660+ } ) ;
661+ vi . stubGlobal ( "fetch" , fetchMock ) ;
662+ vi . mocked ( subscribeDesktopBackendStateChanged ) . mockResolvedValue ( ( ) => { } ) ;
663+
664+ render ( < App /> ) ;
665+
666+ await act ( async ( ) => {
667+ await Promise . resolve ( ) ;
668+ } ) ;
669+
670+ expect ( screen . getByText ( / a c c o u n t s - s y n c : 0 / ) ) . toBeInTheDocument ( ) ;
671+ expect ( vi . mocked ( refreshDesktopTrayState ) ) . toHaveBeenCalledTimes ( 1 ) ;
672+
673+ Object . defineProperty ( document , "visibilityState" , {
674+ configurable : true ,
675+ value : "hidden" ,
676+ } ) ;
677+
678+ await act ( async ( ) => {
679+ vi . advanceTimersByTime ( 5_000 ) ;
680+ await Promise . resolve ( ) ;
681+ } ) ;
682+
683+ expect ( screen . getByText ( / a c c o u n t s - s y n c : 0 / ) ) . toBeInTheDocument ( ) ;
684+ expect ( vi . mocked ( refreshDesktopTrayState ) ) . toHaveBeenCalledTimes ( 1 ) ;
685+ } ) ;
686+
622687 it ( "opens an update modal when the home update indicator is clicked" , async ( ) => {
623688 mockedUpdateService . check . mockResolvedValue ( {
624689 supported : true ,
@@ -1228,6 +1293,9 @@ describe("App", () => {
12281293 render ( < App /> ) ;
12291294
12301295 expect ( await screen . findByText ( / a c c o u n t s - s y n c : 0 / ) ) . toBeInTheDocument ( ) ;
1296+ await waitFor ( ( ) => {
1297+ expect ( subscribeDesktopBackendStateChanged ) . toHaveBeenCalled ( ) ;
1298+ } ) ;
12311299
12321300 await act ( async ( ) => {
12331301 window . dispatchEvent ( new Event ( "online" ) ) ;
@@ -1242,9 +1310,84 @@ describe("App", () => {
12421310 } ) ;
12431311 } ) ;
12441312
1313+ it ( "backs off immediate usage refresh after a failed online recovery" , async ( ) => {
1314+ const fetchMock = vi . fn ( ( input : RequestInfo | URL ) => {
1315+ const url = String ( input ) ;
1316+ if ( url === "http://127.0.0.1:6789/ai-router/api/settings/proxy/status" ) {
1317+ return Promise . resolve ( new Response ( JSON . stringify ( { enabled : false } ) , { status : 200 , headers : { "Content-Type" : "application/json" } } ) ) ;
1318+ }
1319+ if ( url === "http://127.0.0.1:6789/ai-router/api/settings/app" ) {
1320+ return Promise . resolve (
1321+ new Response (
1322+ JSON . stringify ( {
1323+ launch_at_login : false ,
1324+ silent_start : false ,
1325+ close_to_tray : true ,
1326+ show_proxy_switch_on_home : true ,
1327+ show_home_update_indicator : false ,
1328+ status_refresh_interval_seconds : 3600 ,
1329+ usage_request_timeout_seconds : 15 ,
1330+ proxy_host : "127.0.0.1" ,
1331+ proxy_port : 6789 ,
1332+ auto_failover_enabled : true ,
1333+ auto_backup_interval_hours : 24 ,
1334+ backup_retention_count : 10 ,
1335+ audit_limit_message : 200 ,
1336+ audit_limit_function_call : 100 ,
1337+ audit_limit_function_call_output : 100 ,
1338+ audit_limit_reasoning : 40 ,
1339+ audit_limit_custom_tool_call : 100 ,
1340+ audit_limit_custom_tool_call_output : 100 ,
1341+ language : "zh-CN" ,
1342+ theme_mode : "system" ,
1343+ } ) ,
1344+ { status : 200 , headers : { "Content-Type" : "application/json" } } ,
1345+ ) ,
1346+ ) ;
1347+ }
1348+ if ( url === "http://127.0.0.1:6789/ai-router/api/accounts/usage/refresh" ) {
1349+ return Promise . resolve ( new Response ( "vpn unavailable" , { status : 500 } ) ) ;
1350+ }
1351+ return Promise . resolve ( new Response ( null , { status : 404 } ) ) ;
1352+ } ) ;
1353+ vi . stubGlobal ( "fetch" , fetchMock ) ;
1354+ vi . mocked ( subscribeDesktopBackendStateChanged ) . mockResolvedValue ( ( ) => { } ) ;
1355+
1356+ render ( < App /> ) ;
1357+
1358+ expect ( await screen . findByText ( / a c c o u n t s - s y n c : 0 / ) ) . toBeInTheDocument ( ) ;
1359+ vi . useFakeTimers ( ) ;
1360+
1361+ await act ( async ( ) => {
1362+ window . dispatchEvent ( new Event ( "online" ) ) ;
1363+ await vi . advanceTimersByTimeAsync ( 400 ) ;
1364+ } ) ;
1365+
1366+ expect ( fetchMock ) . toHaveBeenCalledWith (
1367+ "http://127.0.0.1:6789/ai-router/api/accounts/usage/refresh" ,
1368+ expect . objectContaining ( { method : "POST" } ) ,
1369+ ) ;
1370+
1371+ const refreshCallsAfterFailure = fetchMock . mock . calls . filter (
1372+ ( [ input ] ) => String ( input ) === "http://127.0.0.1:6789/ai-router/api/accounts/usage/refresh" ,
1373+ ) . length ;
1374+
1375+ await act ( async ( ) => {
1376+ window . dispatchEvent ( new Event ( "online" ) ) ;
1377+ await vi . advanceTimersByTimeAsync ( 400 ) ;
1378+ } ) ;
1379+
1380+ expect (
1381+ fetchMock . mock . calls . filter (
1382+ ( [ input ] ) => String ( input ) === "http://127.0.0.1:6789/ai-router/api/accounts/usage/refresh" ,
1383+ ) ,
1384+ ) . toHaveLength ( refreshCallsAfterFailure ) ;
1385+ } ) ;
1386+
12451387 it ( "triggers an immediate usage refresh when the page becomes visible after a long hidden period" , async ( ) => {
12461388 let currentTime = new Date ( "2026-03-20T09:00:00Z" ) . getTime ( ) ;
12471389 vi . spyOn ( Date , "now" ) . mockImplementation ( ( ) => currentTime ) ;
1390+ const addDocumentEventListenerSpy = vi . spyOn ( document , "addEventListener" ) ;
12481391 vi . stubGlobal (
12491392 "fetch" ,
12501393 vi . fn ( ( input : RequestInfo | URL ) => {
@@ -1295,27 +1438,34 @@ describe("App", () => {
12951438 render ( < App /> ) ;
12961439
12971440 expect ( await screen . findByText ( / a c c o u n t s - s y n c : 0 / ) ) . toBeInTheDocument ( ) ;
1441+ await waitFor ( ( ) => {
1442+ expect ( addDocumentEventListenerSpy ) . toHaveBeenCalledWith ( "visibilitychange" , expect . any ( Function ) ) ;
1443+ } ) ;
1444+ const visibilityChangeHandler = addDocumentEventListenerSpy . mock . calls . toReversed ( ) . find ( ( [ eventName ] ) => eventName === "visibilitychange" ) ?. [ 1 ] as
1445+ | EventListener
1446+ | undefined ;
1447+ expect ( visibilityChangeHandler ) . toEqual ( expect . any ( Function ) ) ;
12981448
12991449 await act ( async ( ) => {
13001450 Object . defineProperty ( document , "visibilityState" , {
13011451 configurable : true ,
13021452 value : "hidden" ,
13031453 } ) ;
1304- document . dispatchEvent ( new Event ( "visibilitychange" ) ) ;
1454+ visibilityChangeHandler ?. ( new Event ( "visibilitychange" ) ) ;
13051455 currentTime += 16_000 ;
13061456 Object . defineProperty ( document , "visibilityState" , {
13071457 configurable : true ,
13081458 value : "visible" ,
13091459 } ) ;
1310- document . dispatchEvent ( new Event ( "visibilitychange" ) ) ;
1460+ visibilityChangeHandler ?. ( new Event ( "visibilitychange" ) ) ;
13111461 } ) ;
13121462
13131463 await waitFor ( ( ) => {
13141464 expect ( fetch ) . toHaveBeenCalledWith (
13151465 "http://127.0.0.1:6789/ai-router/api/accounts/usage/refresh" ,
13161466 expect . objectContaining ( { method : "POST" } ) ,
13171467 ) ;
1318- expect ( screen . getByText ( / a c c o u n t s - s y n c : 1 / ) ) . toBeInTheDocument ( ) ;
1468+ expect ( screen . getByText ( / a c c o u n t s - s y n c : [ 1 - 9 ] / ) ) . toBeInTheDocument ( ) ;
13191469 } ) ;
13201470 } ) ;
13211471
0 commit comments