@@ -85,6 +85,12 @@ type AllEvents = MessengerEvents<AssetsControllerMessenger>;
8585
8686type RootMessenger = Messenger < MockAnyNamespace , AllActions , AllEvents > ;
8787
88+ /** Mirrors the private `RESUBSCRIBE_DEBOUNCE_MS` in AssetsController.ts. */
89+ const RESUBSCRIBE_DEBOUNCE_MS = 250 ;
90+
91+ /** Mirrors the private `RESUBSCRIBE_JITTER_MS` in AssetsController.ts. */
92+ const RESUBSCRIBE_JITTER_MS = 5000 ;
93+
8894const MOCK_ACCOUNT_ID = 'mock-account-id-1' ;
8995const MOCK_ASSET_ID =
9096 'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Caip19AssetId ;
@@ -1392,14 +1398,81 @@ describe('AssetsController', () => {
13921398 } ) ;
13931399
13941400 describe ( 'handleActiveChainsUpdate' , ( ) => {
1395- it ( 're-subscribes assets when chains are added' , async ( ) => {
1401+ it ( 're-subscribes assets when chains are added, debounced and jittered ' , async ( ) => {
13961402 await withController ( async ( { controller } ) => {
1397- const subscribeSpy = jest . spyOn ( controller , 'subscribeAssetsPrice' ) ;
1403+ jest . useFakeTimers ( ) ;
1404+ const randomSpy = jest . spyOn ( Math , 'random' ) . mockReturnValue ( 0.5 ) ;
1405+ try {
1406+ const subscribeSpy = jest . spyOn ( controller , 'subscribeAssetsPrice' ) ;
13981407
1399- const onActiveChainsUpdated = controller . getOnActiveChainsUpdated ( ) ;
1400- onActiveChainsUpdated ( 'TestDataSource' , [ 'eip155:1' ] , [ ] ) ;
1408+ const onActiveChainsUpdated = controller . getOnActiveChainsUpdated ( ) ;
1409+ onActiveChainsUpdated ( 'TestDataSource' , [ 'eip155:1' ] , [ ] ) ;
1410+
1411+ // Re-subscribe is debounced, so it does not run synchronously.
1412+ expect ( subscribeSpy ) . not . toHaveBeenCalled ( ) ;
14011413
1402- expect ( subscribeSpy ) . toHaveBeenCalledTimes ( 1 ) ;
1414+ // Additions are jittered on top of the base debounce, so nothing runs
1415+ // at the base debounce window.
1416+ jest . advanceTimersByTime ( RESUBSCRIBE_DEBOUNCE_MS ) ;
1417+ expect ( subscribeSpy ) . not . toHaveBeenCalled ( ) ;
1418+
1419+ // Once the jitter window elapses it runs exactly once.
1420+ jest . advanceTimersByTime ( RESUBSCRIBE_JITTER_MS ) ;
1421+ expect ( subscribeSpy ) . toHaveBeenCalledTimes ( 1 ) ;
1422+ } finally {
1423+ randomSpy . mockRestore ( ) ;
1424+ jest . useRealTimers ( ) ;
1425+ }
1426+ } ) ;
1427+ } ) ;
1428+
1429+ it ( 're-subscribes promptly (no jitter) when a chain removal pre-empts a pending jittered addition' , async ( ) => {
1430+ await withController ( async ( { controller } ) => {
1431+ jest . useFakeTimers ( ) ;
1432+ const randomSpy = jest . spyOn ( Math , 'random' ) . mockReturnValue ( 0.9 ) ;
1433+ try {
1434+ const subscribeSpy = jest . spyOn ( controller , 'subscribeAssetsPrice' ) ;
1435+
1436+ const onActiveChainsUpdated = controller . getOnActiveChainsUpdated ( ) ;
1437+ // Addition schedules a jittered re-subscribe...
1438+ onActiveChainsUpdated ( 'TestDataSource' , [ 'eip155:1' ] , [ ] ) ;
1439+ // ...then a removal arrives; it must pre-empt to the prompt window.
1440+ onActiveChainsUpdated ( 'TestDataSource' , [ ] , [ 'eip155:137' ] ) ;
1441+
1442+ jest . advanceTimersByTime ( RESUBSCRIBE_DEBOUNCE_MS ) ;
1443+ expect ( subscribeSpy ) . toHaveBeenCalledTimes ( 1 ) ;
1444+ } finally {
1445+ randomSpy . mockRestore ( ) ;
1446+ jest . useRealTimers ( ) ;
1447+ }
1448+ } ) ;
1449+ } ) ;
1450+
1451+ it ( 'coalesces a burst of active-chain updates into a single re-subscribe' , async ( ) => {
1452+ await withController ( async ( { controller } ) => {
1453+ jest . useFakeTimers ( ) ;
1454+ try {
1455+ const subscribeSpy = jest . spyOn ( controller , 'subscribeAssetsPrice' ) ;
1456+
1457+ const onActiveChainsUpdated = controller . getOnActiveChainsUpdated ( ) ;
1458+ // Simulate a burst of chain up/down notifications within the window.
1459+ onActiveChainsUpdated ( 'TestDataSource' , [ 'eip155:1' ] , [ ] ) ;
1460+ onActiveChainsUpdated (
1461+ 'TestDataSource' ,
1462+ [ 'eip155:1' , 'eip155:137' ] ,
1463+ [ 'eip155:1' ] ,
1464+ ) ;
1465+ onActiveChainsUpdated (
1466+ 'TestDataSource' ,
1467+ [ 'eip155:137' ] ,
1468+ [ 'eip155:1' , 'eip155:137' ] ,
1469+ ) ;
1470+
1471+ jest . advanceTimersByTime ( RESUBSCRIBE_DEBOUNCE_MS ) ;
1472+ expect ( subscribeSpy ) . toHaveBeenCalledTimes ( 1 ) ;
1473+ } finally {
1474+ jest . useRealTimers ( ) ;
1475+ }
14031476 } ) ;
14041477 } ) ;
14051478
@@ -1428,12 +1501,18 @@ describe('AssetsController', () => {
14281501
14291502 it ( 're-subscribes assets when chains are removed' , async ( ) => {
14301503 await withController ( async ( { controller } ) => {
1431- const subscribeSpy = jest . spyOn ( controller , 'subscribeAssetsPrice' ) ;
1504+ jest . useFakeTimers ( ) ;
1505+ try {
1506+ const subscribeSpy = jest . spyOn ( controller , 'subscribeAssetsPrice' ) ;
14321507
1433- const onActiveChainsUpdated = controller . getOnActiveChainsUpdated ( ) ;
1434- onActiveChainsUpdated ( 'TestDataSource' , [ ] , [ 'eip155:1' ] ) ;
1508+ const onActiveChainsUpdated = controller . getOnActiveChainsUpdated ( ) ;
1509+ onActiveChainsUpdated ( 'TestDataSource' , [ ] , [ 'eip155:1' ] ) ;
14351510
1436- expect ( subscribeSpy ) . toHaveBeenCalledTimes ( 1 ) ;
1511+ jest . advanceTimersByTime ( RESUBSCRIBE_DEBOUNCE_MS ) ;
1512+ expect ( subscribeSpy ) . toHaveBeenCalledTimes ( 1 ) ;
1513+ } finally {
1514+ jest . useRealTimers ( ) ;
1515+ }
14371516 } ) ;
14381517 } ) ;
14391518
@@ -2019,8 +2098,19 @@ describe('AssetsController', () => {
20192098 } ;
20202099
20212100 await withController (
2022- { state : initialState } ,
2101+ { state : initialState , clientControllerState : { isUiOpen : true } } ,
20232102 async ( { controller, messenger } ) => {
2103+ // UI must be open and keyring unlocked so AccountActivityDataSource is
2104+ // subscribed and can route balanceUpdated events to the account.
2105+ (
2106+ messenger as unknown as {
2107+ publish : ( topic : string , payload ?: unknown ) => void ;
2108+ }
2109+ ) . publish ( 'ClientController:stateChange' , { isUiOpen : true } ) ;
2110+ messenger . publish ( 'KeyringController:unlock' ) ;
2111+
2112+ await flushPromises ( ) ;
2113+
20242114 messenger . publish ( 'AccountActivityService:balanceUpdated' , {
20252115 address : '0x1234567890123456789012345678901234567890' ,
20262116 chain : 'eip155:42161' ,
0 commit comments