Skip to content

Commit d578dbb

Browse files
authored
Merge pull request #1754 from AmbireTech/fixes/continuous-updates-tests
Fixes / ContinuousUpdatesController tests
2 parents 114d28f + e771564 commit d578dbb

File tree

5 files changed

+152
-57
lines changed

5 files changed

+152
-57
lines changed

src/classes/recurringTimeout/recurringTimeout.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface IRecurringTimeout {
1212
updateTimeout: (options: { timeout: number }) => void
1313
running: boolean
1414
sessionId: number
15+
fnExecutionsCount: number
1516
startedRunningAt: number
1617
currentTimeout: number
1718
promise: Promise<void> | undefined
@@ -30,6 +31,8 @@ export class RecurringTimeout implements IRecurringTimeout {
3031
// used mainly for testing how many times the fn was called
3132
sessionId: number = 0
3233

34+
fnExecutionsCount: number = 0
35+
3336
running = false
3437

3538
startedRunningAt: number = 0
@@ -64,6 +67,7 @@ export class RecurringTimeout implements IRecurringTimeout {
6467
}
6568

6669
stop() {
70+
this.startScheduled = false
6771
this.#reset()
6872
}
6973

@@ -73,10 +77,9 @@ export class RecurringTimeout implements IRecurringTimeout {
7377
}
7478

7579
async #loop() {
76-
if (this.promise) return // prevents multiple executions in one tick
77-
7880
try {
7981
this.promise = this.#fn()
82+
this.fnExecutionsCount += 1
8083
await this.promise
8184
} catch (err: any) {
8285
if (!this.promise) return
@@ -113,6 +116,8 @@ export class RecurringTimeout implements IRecurringTimeout {
113116

114117
if (newTimeout) this.updateTimeout({ timeout: newTimeout })
115118

119+
if (this.promise) return // prevents multiple executions in one tick
120+
116121
if (runImmediately) {
117122
this.#loop()
118123
} else {
@@ -123,8 +128,6 @@ export class RecurringTimeout implements IRecurringTimeout {
123128

124129
#reset() {
125130
this.running = false
126-
this.promise = undefined
127-
this.startScheduled = false
128131
this.startedRunningAt = 0
129132

130133
if (this.#timeoutId) {

src/controllers/accounts/accounts.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ export class AccountsController extends EventEmitter implements IAccountsControl
4949
// Holds the initial load promise, so that one can wait until it completes
5050
initialLoadPromise?: Promise<void>
5151

52+
// Tracks the initial load of account states. Unlike `initialLoadPromise`,
53+
// this one isn’t awaited during the AccountsController initial load, so it’s the only
54+
// reliable way to know when account states are fully loaded.
55+
accountStatesInitialLoadPromise?: Promise<void>
56+
5257
constructor(
5358
storage: IStorageController,
5459
providers: IProvidersController,
@@ -100,9 +105,11 @@ export class AccountsController extends EventEmitter implements IAccountsControl
100105
// NOTE: YOU MUST USE waitForAccountsCtrlFirstLoad IN TESTS
101106
// TO ENSURE ACCOUNT STATE IS LOADED
102107
// eslint-disable-next-line @typescript-eslint/no-floating-promises
103-
this.#updateAccountStates(
108+
this.accountStatesInitialLoadPromise = this.#updateAccountStates(
104109
this.#getAccountsToUpdateAccountStatesInBackground(initialSelectedAccountAddr)
105-
)
110+
).finally(() => {
111+
this.accountStatesInitialLoadPromise = undefined
112+
})
106113
}
107114

108115
async updateAccountStates(

src/controllers/continuousUpdates/continuousUpdates.test.ts

Lines changed: 123 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
/* eslint-disable no-await-in-loop */
2-
/* eslint-disable prettier/prettier */
32
import fetch from 'node-fetch'
43

4+
/* eslint-disable prettier/prettier */
55
import { relayerUrl, velcroUrl } from '../../../test/config'
66
import { produceMemoryStore } from '../../../test/helpers'
77
import { mockUiManager } from '../../../test/helpers/ui'
88
import { waitForFnToBeCalledAndExecuted } from '../../../test/recurringTimeout'
99
import { ACCOUNT_STATE_PENDING_INTERVAL } from '../../consts/intervals'
10+
import { RPCProviders } from '../../interfaces/provider'
1011
import { SubmittedAccountOp } from '../../libs/accountOp/submittedAccountOp'
1112
import { KeystoreSigner } from '../../libs/keystoreSigner/keystoreSigner'
13+
import wait from '../../utils/wait'
1214
import { MainController } from '../main/main'
1315

1416
// Public API key, shared by Socket, for testing purposes only
@@ -77,6 +79,12 @@ const submittedAccountOp = {
7779
}
7880
} as SubmittedAccountOp
7981

82+
function filterProviders(providers: RPCProviders, chainIdsToFilter: string[] = []): RPCProviders {
83+
return Object.fromEntries(
84+
Object.entries(providers).filter(([key]) => chainIdsToFilter.includes(key))
85+
) as RPCProviders
86+
}
87+
8088
const prepareTest = async () => {
8189
const storage = produceMemoryStore()
8290
await storage.set('accounts', accounts)
@@ -96,10 +104,16 @@ const prepareTest = async () => {
96104
velcroUrl
97105
})
98106
mainCtrl.portfolio.updateSelectedAccount = jest.fn().mockResolvedValue(undefined)
99-
mainCtrl.updateSelectedAccountPortfolio = jest.fn().mockResolvedValue(undefined)
107+
mainCtrl.updateSelectedAccountPortfolio = jest.fn().mockImplementation(async () => {
108+
await wait(500)
109+
})
100110
mainCtrl.domains.reverseLookup = jest.fn().mockResolvedValue(undefined)
101-
mainCtrl.accounts.updateAccountStates = jest.fn().mockResolvedValue(undefined)
102-
mainCtrl.accounts.updateAccountState = jest.fn().mockResolvedValue(undefined)
111+
mainCtrl.accounts.updateAccountStates = jest.fn().mockImplementation(async () => {
112+
await wait(500)
113+
})
114+
mainCtrl.accounts.updateAccountState = jest.fn().mockImplementation(async () => {
115+
await wait(500)
116+
})
103117
mainCtrl.updateAccountsOpsStatuses = jest.fn().mockResolvedValue({ newestOpTimestamp: 0 })
104118

105119
return { mainCtrl }
@@ -121,6 +135,14 @@ const waitForContinuousUpdatesCtrlReady = async (mainCtrl: MainController) => {
121135
}
122136
}
123137

138+
const waitForAccountStatesInitialLoad = async (mainCtrl: MainController) => {
139+
await jest.advanceTimersByTimeAsync(0)
140+
141+
while (mainCtrl.accounts.accountStatesInitialLoadPromise) {
142+
await jest.advanceTimersByTimeAsync(20)
143+
}
144+
}
145+
124146
describe('ContinuousUpdatesController intervals', () => {
125147
beforeEach(() => {
126148
jest.useFakeTimers()
@@ -135,29 +157,46 @@ describe('ContinuousUpdatesController intervals', () => {
135157
test('should run updatePortfolioInterval', async () => {
136158
const { mainCtrl } = await prepareTest()
137159
await waitForContinuousUpdatesCtrlReady(mainCtrl)
160+
await waitForAccountStatesInitialLoad(mainCtrl)
161+
const providersForTesting = ['1', '137']
162+
const mockedProviders = filterProviders(mainCtrl.providers.providers, providersForTesting)
163+
// ensure all providers are working
164+
mockedProviders[1].isWorking = true
165+
mockedProviders[137].isWorking = true
166+
mainCtrl.providers.providers = mockedProviders
167+
138168
jest.spyOn(mainCtrl.continuousUpdates.updatePortfolioInterval, 'restart')
139-
const updatePortfolioSpy = jest.spyOn(mainCtrl.continuousUpdates, 'updatePortfolio')
140169
mainCtrl.ui.addView({ id: '1', type: 'popup', currentRoute: 'dashboard', isReady: true })
141170
await jest.advanceTimersByTimeAsync(0)
142171
expect(mainCtrl.continuousUpdates.updatePortfolioInterval.restart).toHaveBeenCalled()
143172
const updateSelectedAccountPortfolioSpy = jest.spyOn(mainCtrl, 'updateSelectedAccountPortfolio')
173+
const initialFnExecutionsCount =
174+
mainCtrl.continuousUpdates.updatePortfolioInterval.fnExecutionsCount
144175
await waitForFnToBeCalledAndExecuted(mainCtrl.continuousUpdates.updatePortfolioInterval)
145-
expect(updatePortfolioSpy).toHaveBeenCalledTimes(1)
176+
expect(mainCtrl.continuousUpdates.updatePortfolioInterval.fnExecutionsCount).toBe(
177+
initialFnExecutionsCount + 1
178+
)
146179
const updateSelectedAccountCalledTimes = updateSelectedAccountPortfolioSpy.mock.calls.length
147180
await mainCtrl.activity.addAccountOp(submittedAccountOp)
148181
await jest.advanceTimersByTimeAsync(0)
149182
await waitForFnToBeCalledAndExecuted(mainCtrl.continuousUpdates.updatePortfolioInterval)
150-
expect(updatePortfolioSpy).toHaveBeenCalledTimes(2)
183+
expect(mainCtrl.continuousUpdates.updatePortfolioInterval.fnExecutionsCount).toBe(
184+
initialFnExecutionsCount + 2
185+
)
151186
expect(updateSelectedAccountPortfolioSpy).toHaveBeenCalledTimes(
152187
updateSelectedAccountCalledTimes
153188
) // tests the branching in the updatePortfolio func
154189
mainCtrl.ui.removeView('1')
155190
await jest.advanceTimersByTimeAsync(0)
156191
expect(mainCtrl.continuousUpdates.updatePortfolioInterval.restart).toHaveBeenCalledTimes(2)
157192
await waitForFnToBeCalledAndExecuted(mainCtrl.continuousUpdates.updatePortfolioInterval)
158-
expect(updatePortfolioSpy).toHaveBeenCalledTimes(3)
193+
expect(mainCtrl.continuousUpdates.updatePortfolioInterval.fnExecutionsCount).toBe(
194+
initialFnExecutionsCount + 3
195+
)
159196
await waitForFnToBeCalledAndExecuted(mainCtrl.continuousUpdates.updatePortfolioInterval)
160-
expect(updatePortfolioSpy).toHaveBeenCalledTimes(4)
197+
expect(mainCtrl.continuousUpdates.updatePortfolioInterval.fnExecutionsCount).toBe(
198+
initialFnExecutionsCount + 4
199+
)
161200
})
162201

163202
test('should run accountsOpsStatusesInterval', async () => {
@@ -166,18 +205,22 @@ describe('ContinuousUpdatesController intervals', () => {
166205

167206
jest.spyOn(mainCtrl.continuousUpdates.accountsOpsStatusesInterval, 'start')
168207
jest.spyOn(mainCtrl.continuousUpdates.accountsOpsStatusesInterval, 'stop')
169-
const updateAccountsOpsStatuses = jest.spyOn(
170-
mainCtrl.continuousUpdates,
171-
'updateAccountsOpsStatuses'
172-
)
173208

174209
await mainCtrl.activity.addAccountOp(submittedAccountOp)
175210
await jest.advanceTimersByTimeAsync(0)
211+
212+
const initialFnExecutionsCount =
213+
mainCtrl.continuousUpdates.accountsOpsStatusesInterval.fnExecutionsCount
214+
176215
expect(mainCtrl.continuousUpdates.accountsOpsStatusesInterval.start).toHaveBeenCalled()
177216
await waitForFnToBeCalledAndExecuted(mainCtrl.continuousUpdates.accountsOpsStatusesInterval)
178-
expect(updateAccountsOpsStatuses).toHaveBeenCalledTimes(1)
217+
expect(mainCtrl.continuousUpdates.accountsOpsStatusesInterval.fnExecutionsCount).toBe(
218+
initialFnExecutionsCount + 1
219+
)
179220
await waitForFnToBeCalledAndExecuted(mainCtrl.continuousUpdates.accountsOpsStatusesInterval)
180-
expect(updateAccountsOpsStatuses).toHaveBeenCalledTimes(2)
221+
expect(mainCtrl.continuousUpdates.accountsOpsStatusesInterval.fnExecutionsCount).toBe(
222+
initialFnExecutionsCount + 2
223+
)
181224
jest.spyOn(mainCtrl.activity, 'broadcastedButNotConfirmed', 'get').mockReturnValue([])
182225
// @ts-ignore
183226
mainCtrl.activity.emitUpdate()
@@ -191,21 +234,21 @@ describe('ContinuousUpdatesController intervals', () => {
191234
jest.spyOn(mainCtrl.continuousUpdates.accountStateLatestInterval, 'restart')
192235
jest.spyOn(mainCtrl.continuousUpdates.accountStatePendingInterval, 'start')
193236
jest.spyOn(mainCtrl.continuousUpdates.accountStatePendingInterval, 'stop')
194-
const updateAccountStateLatestMock = jest.spyOn(
195-
mainCtrl.continuousUpdates,
196-
'updateAccountStateLatest'
197-
)
198-
const updateAccountStatePendingMock = jest.spyOn(
199-
mainCtrl.continuousUpdates,
200-
'updateAccountStatePending'
201-
)
202237

203238
await waitForContinuousUpdatesCtrlReady(mainCtrl)
204239

240+
const initialAccountStateLatestFnExecutionsCount =
241+
mainCtrl.continuousUpdates.accountStateLatestInterval.fnExecutionsCount
242+
243+
const initialAccountStatePendingFnExecutionsCount =
244+
mainCtrl.continuousUpdates.accountStateLatestInterval.fnExecutionsCount
245+
205246
expect(mainCtrl.continuousUpdates.accountStateLatestInterval.running).toBe(true)
206247
expect(mainCtrl.continuousUpdates.accountStatePendingInterval.running).toBe(false)
207248
await waitForFnToBeCalledAndExecuted(mainCtrl.continuousUpdates.accountStateLatestInterval)
208-
expect(updateAccountStateLatestMock).toHaveBeenCalledTimes(1)
249+
expect(mainCtrl.continuousUpdates.accountStateLatestInterval.fnExecutionsCount).toBe(
250+
initialAccountStateLatestFnExecutionsCount + 1
251+
)
209252
mainCtrl.statuses.signAndBroadcastAccountOp = 'SUCCESS'
210253
// @ts-ignore
211254
mainCtrl.emitUpdate()
@@ -217,36 +260,76 @@ describe('ContinuousUpdatesController intervals', () => {
217260
expect(mainCtrl.continuousUpdates.accountStatePendingInterval.currentTimeout).toBe(
218261
ACCOUNT_STATE_PENDING_INTERVAL / 2
219262
)
263+
220264
await waitForFnToBeCalledAndExecuted(mainCtrl.continuousUpdates.accountStatePendingInterval)
221-
expect(updateAccountStatePendingMock).toHaveBeenCalledTimes(1)
265+
expect(mainCtrl.continuousUpdates.accountStatePendingInterval.fnExecutionsCount).toBe(
266+
initialAccountStatePendingFnExecutionsCount + 1
267+
)
222268
expect(mainCtrl.continuousUpdates.accountStateLatestInterval.restart).toHaveBeenCalledTimes(2)
223269
expect(mainCtrl.continuousUpdates.accountStatePendingInterval.stop).toHaveBeenCalledTimes(1)
224270
})
225271

226272
test('should run fastAccountStateReFetchTimeout', async () => {
227273
const { mainCtrl } = await prepareTest()
228274
await waitForContinuousUpdatesCtrlReady(mainCtrl)
275+
await waitForAccountStatesInitialLoad(mainCtrl)
229276

277+
const providersForTesting = ['1', '137']
278+
const mockedProviders = filterProviders(mainCtrl.providers.providers, providersForTesting)
279+
// ensure there is at least one provider that is not working
280+
mockedProviders[1].isWorking = false
281+
mockedProviders[137].isWorking = true
282+
mainCtrl.providers.providers = mockedProviders
230283
jest.spyOn(mainCtrl.continuousUpdates.fastAccountStateReFetchTimeout, 'start')
231-
jest.spyOn(mainCtrl.continuousUpdates, 'updateAccountStateLatest')
232-
jest.spyOn(mainCtrl.continuousUpdates, 'updateAccountStatePending')
233-
234-
const fastAccountStateReFetchMock = jest.spyOn(
235-
mainCtrl.continuousUpdates,
236-
'fastAccountStateReFetch'
237-
)
284+
mainCtrl.continuousUpdates.accountStateLatestInterval.start = jest
285+
.fn()
286+
.mockResolvedValue(undefined)
287+
mainCtrl.continuousUpdates.accountStateLatestInterval.restart = jest
288+
.fn()
289+
.mockResolvedValue(undefined)
290+
mainCtrl.continuousUpdates.accountStatePendingInterval.start = jest
291+
.fn()
292+
.mockResolvedValue(undefined)
293+
mainCtrl.continuousUpdates.accountStatePendingInterval.restart = jest
294+
.fn()
295+
.mockResolvedValue(undefined)
238296

297+
// ensure there is at least one provider that is not working
298+
mainCtrl.providers.providers[1].isWorking = false
299+
mainCtrl.providers.providers[137].isWorking = true
239300
mainCtrl.ui.addView({ id: '1', type: 'popup', currentRoute: 'dashboard', isReady: true })
240-
await jest.advanceTimersByTimeAsync(0)
301+
const initialFnExecutionsCount =
302+
mainCtrl.continuousUpdates.fastAccountStateReFetchTimeout.fnExecutionsCount
241303
expect(mainCtrl.continuousUpdates.fastAccountStateReFetchTimeout.start).toHaveBeenCalledTimes(1)
242-
expect(fastAccountStateReFetchMock).toHaveBeenCalledTimes(0)
243-
// ensure there is at least one provider that is not working
244-
if (Object.values(mainCtrl.providers.providers).some((p) => !p.isWorking)) {
245-
mainCtrl.providers.providers[1].isWorking = false
246-
}
304+
expect(mainCtrl.continuousUpdates.fastAccountStateReFetchTimeout.fnExecutionsCount).toBe(
305+
initialFnExecutionsCount
306+
)
307+
// @ts-ignore
308+
mainCtrl.providers.emitUpdate()
309+
// @ts-ignore
310+
mainCtrl.providers.emitUpdate()
311+
// @ts-ignore
312+
mainCtrl.providers.emitUpdate()
313+
247314
await waitForFnToBeCalledAndExecuted(mainCtrl.continuousUpdates.fastAccountStateReFetchTimeout)
248-
expect(fastAccountStateReFetchMock).toHaveBeenCalledTimes(1)
315+
// @ts-ignore
316+
mainCtrl.providers.emitUpdate()
317+
// @ts-ignore
318+
mainCtrl.providers.emitUpdate()
319+
320+
expect(mainCtrl.continuousUpdates.fastAccountStateReFetchTimeout.fnExecutionsCount).toBe(
321+
initialFnExecutionsCount + 1
322+
)
323+
// @ts-ignore
324+
mainCtrl.providers.emitUpdate()
325+
// @ts-ignore
326+
mainCtrl.providers.emitUpdate()
327+
// @ts-ignore
328+
mainCtrl.providers.emitUpdate()
329+
249330
await waitForFnToBeCalledAndExecuted(mainCtrl.continuousUpdates.fastAccountStateReFetchTimeout)
250-
expect(fastAccountStateReFetchMock).toHaveBeenCalledTimes(2)
331+
expect(mainCtrl.continuousUpdates.fastAccountStateReFetchTimeout.fnExecutionsCount).toBe(
332+
initialFnExecutionsCount + 2
333+
)
251334
})
252335
})

0 commit comments

Comments
 (0)