diff --git a/integration-tests/cheqd-credentials.test.ts b/integration-tests/cheqd-credentials.test.ts index 9e15e2e4..304e65ef 100644 --- a/integration-tests/cheqd-credentials.test.ts +++ b/integration-tests/cheqd-credentials.test.ts @@ -1,7 +1,7 @@ import { IWallet } from '@docknetwork/wallet-sdk-core/lib/types'; import { createVerificationController } from '@docknetwork/wallet-sdk-core/src/verification-controller'; import { CheqdCredentialNonZKP, CheqdCredentialZKP } from './data/credentials/cheqd-credentials'; -import { closeWallet, createNewWallet, getCredentialProvider, getWallet } from './helpers'; +import { addCredentialIfNotExists, closeWallet, createNewWallet, getCredentialProvider, getWallet } from './helpers'; import { ProofTemplateIds, createProofRequest } from './helpers/certs-helpers'; describe('Cheq integration tests', () => { @@ -12,7 +12,7 @@ describe('Cheq integration tests', () => { it('should verify a non ZKP cheqd credential', async () => { const wallet: IWallet = await getWallet(); - getCredentialProvider().addCredential(CheqdCredentialNonZKP); + await addCredentialIfNotExists(CheqdCredentialNonZKP); const proofRequest = await createProofRequest( ProofTemplateIds.ANY_CREDENTIAL, diff --git a/integration-tests/credentials.test.ts b/integration-tests/credentials.test.ts index c786ba46..4b75cfd8 100644 --- a/integration-tests/credentials.test.ts +++ b/integration-tests/credentials.test.ts @@ -5,6 +5,7 @@ import { UniversityDegreeCredentialBBS, } from './data/credentials'; import { + addCredentialIfNotExists, cleanup, closeWallet, getCredentialProvider, @@ -29,7 +30,7 @@ describe('Credentials', () => { it('expect to import credentials', async () => { for (const credentialJSON of allCredentials) { - await getCredentialProvider().addCredential(credentialJSON); + await addCredentialIfNotExists(credentialJSON); const credential = await getCredentialProvider().getById( credentialJSON.id, ); @@ -46,7 +47,7 @@ describe('Credentials', () => { `${credentialUrl}?p=${btoa(password)}`, ); - await getCredentialProvider().addCredential(credential); + await addCredentialIfNotExists(credential); const result: any = await getCredentialProvider().isValid(credential); @@ -54,7 +55,7 @@ describe('Credentials', () => { }); it('should get status of bbs revokable credential - cheqd issuer', async () => { - await getCredentialProvider().addCredential(CheqdRevocationCredential); + await addCredentialIfNotExists(CheqdRevocationCredential); const result: any = await getCredentialProvider().isValid( CheqdRevocationCredential, diff --git a/integration-tests/helpers/wallet-helpers.ts b/integration-tests/helpers/wallet-helpers.ts index ef0339fa..3ae2de1f 100644 --- a/integration-tests/helpers/wallet-helpers.ts +++ b/integration-tests/helpers/wallet-helpers.ts @@ -86,6 +86,14 @@ export function getCredentialProvider(): ICredentialProvider { return credentialProvider; } +export async function addCredentialIfNotExists(credential: any) { + try { + return await credentialProvider.addCredential(credential); + } catch (err) { + if (!err.message?.includes('already exists')) throw err; + } +} + export async function setNetwork(networkId) { return Promise.resolve(wallet.setNetwork(networkId)); } @@ -122,15 +130,24 @@ export async function getDocumentsByType(type) { return wallet.getDocumentsByType(type); } -export async function closeWallet(wallet?: IWallet) { - if (!wallet) { - wallet = await getWallet(); +export async function closeWallet(walletToClose?: IWallet) { + if (!walletToClose) { + walletToClose = await getWallet(); + } + + if (messageProvider) { + messageProvider.stop(); + } + + if (walletToClose.networkCheckInterval) { + clearInterval(walletToClose.networkCheckInterval); + walletToClose.networkCheckInterval = undefined; } return new Promise(res => { setTimeout(async () => { try { - wallet.dataStore.db.destroy(); + walletToClose.dataStore.db.destroy(); await blockchainService.disconnect(); } catch (err) { console.error(err); diff --git a/integration-tests/sd-jwt.test.ts b/integration-tests/sd-jwt.test.ts index e5087c25..7215703a 100644 --- a/integration-tests/sd-jwt.test.ts +++ b/integration-tests/sd-jwt.test.ts @@ -6,6 +6,7 @@ import { UniversityDegreeCredentialBBS, } from './data/credentials'; import { + addCredentialIfNotExists, cleanup, closeWallet, getCredentialProvider, @@ -26,8 +27,8 @@ describe('SD JWT Credentials', () => { wallet = await getWallet(); - const result = await getCredentialProvider().addCredential(jwt); - credentialId = result.id; + const result = await addCredentialIfNotExists(jwt); + credentialId = result?.id; }); it('expect to import SD-JWT credential', async () => { diff --git a/integration-tests/switch-wallet.test.ts b/integration-tests/switch-wallet.test.ts index 644d9e8d..96f48a52 100644 --- a/integration-tests/switch-wallet.test.ts +++ b/integration-tests/switch-wallet.test.ts @@ -1,6 +1,6 @@ import { + addCredentialIfNotExists, closeWallet, - getCredentialProvider, getDocumentsByType, getWallet, setNetwork, @@ -17,7 +17,7 @@ describe('Switch wallet', () => { it('expect to maintain separate document stores when switching between networks', async () => { await setNetwork('testnet'); - await getCredentialProvider().addCredential(BasicCredential); + await addCredentialIfNotExists(BasicCredential); const testnetCredentials = await getDocumentsByType('VerifiableCredential'); expect(testnetCredentials.length).toBe(1); diff --git a/integration-tests/verification-flow/bbs-plus-revocation.test.ts b/integration-tests/verification-flow/bbs-plus-revocation.test.ts index bf105fc2..90a644ba 100644 --- a/integration-tests/verification-flow/bbs-plus-revocation.test.ts +++ b/integration-tests/verification-flow/bbs-plus-revocation.test.ts @@ -1,5 +1,6 @@ import {IWallet} from '@docknetwork/wallet-sdk-core/lib/types'; import { + addCredentialIfNotExists, closeWallet, getCredentialProvider, getWallet, @@ -12,7 +13,7 @@ describe('BBS+ revocation', () => { it('should verify a revokable bbs+ credential', async () => { const wallet: IWallet = await getWallet(); - getCredentialProvider().addCredential(bbsPlusRevocationCredential); + await addCredentialIfNotExists(bbsPlusRevocationCredential); const proofRequest = await createProofRequest( ProofTemplateIds.ANY_CREDENTIAL, @@ -59,7 +60,7 @@ describe('BBS+ revocation', () => { it('should verify a revokable bbs+ credential with an updated witness', async () => { const wallet: IWallet = await getWallet(); - getCredentialProvider().addCredential(credentialWithUpdatedWitness); + await addCredentialIfNotExists(credentialWithUpdatedWitness); const proofRequest = await createProofRequest( ProofTemplateIds.ANY_CREDENTIAL, diff --git a/integration-tests/verification-flow/cheqd-revocation.test.ts b/integration-tests/verification-flow/cheqd-revocation.test.ts index 8dd581c7..d0c42687 100644 --- a/integration-tests/verification-flow/cheqd-revocation.test.ts +++ b/integration-tests/verification-flow/cheqd-revocation.test.ts @@ -1,5 +1,6 @@ import {IWallet} from '@docknetwork/wallet-sdk-core/lib/types'; import { + addCredentialIfNotExists, closeWallet, getCredentialProvider, getWallet, @@ -12,7 +13,7 @@ describe('BBS+ revocation cheqd', () => { it('should verify a revokable bbs+ credential issued on cheqd', async () => { const wallet: IWallet = await getWallet(); - getCredentialProvider().addCredential(cheqdRevocationCredential); + await addCredentialIfNotExists(cheqdRevocationCredential); const proofRequest = await createProofRequest( ProofTemplateIds.ANY_CREDENTIAL, diff --git a/integration-tests/verification-flow/range-proofs.test.ts b/integration-tests/verification-flow/range-proofs.test.ts index a304e02b..9e56ed95 100644 --- a/integration-tests/verification-flow/range-proofs.test.ts +++ b/integration-tests/verification-flow/range-proofs.test.ts @@ -1,7 +1,7 @@ import {IWallet} from '@docknetwork/wallet-sdk-core/lib/types'; import { + addCredentialIfNotExists, closeWallet, - getCredentialProvider, getWallet, } from '../helpers/wallet-helpers'; import {createVerificationController} from '@docknetwork/wallet-sdk-core/src/verification-controller'; @@ -29,7 +29,7 @@ describe('Range proofs verification', () => { `${credentialUrl}?p=${btoa(password)}`, ); - getCredentialProvider().addCredential(credential); + await addCredentialIfNotExists(credential); await controller.start({ template: proofRequest.qr, @@ -80,7 +80,7 @@ describe('Range proofs verification', () => { `${credentialUrl}?p=${btoa(password)}`, ); - getCredentialProvider().addCredential(credential); + await addCredentialIfNotExists(credential); await controller.start({ template: proofRequest.qr, @@ -135,11 +135,7 @@ describe('Range proofs verification', () => { `${credentialUrl}?p=${btoa(password)}`, ); - try { - await getCredentialProvider().addCredential(credential); - } catch(err) { - console.error('Credential already added'); - } + await addCredentialIfNotExists(credential); // pexToBounds should skip issuanceDate // There is an SDK limitation that prevents us from sharing the actual issuanceDate diff --git a/integration-tests/verification-flow/vpi-verification.test.ts b/integration-tests/verification-flow/vpi-verification.test.ts index 50592bdd..1810b0e8 100644 --- a/integration-tests/verification-flow/vpi-verification.test.ts +++ b/integration-tests/verification-flow/vpi-verification.test.ts @@ -1,5 +1,6 @@ import {IWallet} from '@docknetwork/wallet-sdk-core/lib/types'; import { + addCredentialIfNotExists, closeWallet, getCredentialProvider, getWallet, @@ -75,7 +76,7 @@ describe('VPI verification', () => { it('should verify a vpi credential', async () => { const wallet: IWallet = await getWallet(); - getCredentialProvider().addCredential(credential); + await addCredentialIfNotExists(credential); const proofRequest = await createProofRequest( ProofTemplateIds.ANY_CREDENTIAL, diff --git a/packages/core/src/message-provider.ts b/packages/core/src/message-provider.ts index b4964507..d486237b 100644 --- a/packages/core/src/message-provider.ts +++ b/packages/core/src/message-provider.ts @@ -243,6 +243,8 @@ export function createMessageProvider({ } let listenerIntervalId = null; + let processMessageTimeoutId = null; + let stopped = false; const processMessageInterval = 3000; @@ -250,7 +252,21 @@ export function createMessageProvider({ try { await processDIDCommMessages(); } finally { - setTimeout(processMessageRecurrentJob, processMessageInterval); + if (!stopped) { + processMessageTimeoutId = setTimeout(processMessageRecurrentJob, processMessageInterval); + } + } + } + + function stop() { + stopped = true; + if (listenerIntervalId) { + clearInterval(listenerIntervalId); + listenerIntervalId = null; + } + if (processMessageTimeoutId) { + clearTimeout(processMessageTimeoutId); + processMessageTimeoutId = null; } } @@ -333,12 +349,20 @@ export function createMessageProvider({ */ startAutoFetch(timeout = 2000) { clearInterval(listenerIntervalId); + stopped = false; listenerIntervalId = setInterval(async () => { - await fetchMessages(); - await processDIDCommMessages(); + try { + await fetchMessages(); + await processDIDCommMessages(); + } catch (err) { + logger.debug(`Auto-fetch error: ${err.message}`); + } }, timeout); - return () => clearInterval(listenerIntervalId); + return () => { + clearInterval(listenerIntervalId); + listenerIntervalId = null; + }; }, /** * Clears all cached messages from the wallet @@ -410,5 +434,6 @@ export function createMessageProvider({ * console.log('Message marked as read'); */ markMessageAsRead, + stop, } as any; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 0944ac47..4ca7d9d6 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -478,6 +478,11 @@ export interface IMessageProvider { * @returns {Promise} */ processMessageRecurrentJob: () => Promise; + + /** + * Stops all message fetching and processing timers + */ + stop: () => void; } /** diff --git a/packages/react-native/lib/message-handler.js b/packages/react-native/lib/message-handler.js index eac8d888..a06b595a 100644 --- a/packages/react-native/lib/message-handler.js +++ b/packages/react-native/lib/message-handler.js @@ -7,6 +7,85 @@ import { import {Logger} from '@docknetwork/wallet-sdk-wasm/src/core/logger'; import rnRpcServer from './rn-rpc-server'; +export class MessageDispatcher { + constructor(getWebView) { + this.getWebView = getWebView; + this.queue = []; + this.intervalId = null; + this.isProcessing = false; + + this._startProcessor(); + } + + dispatch(type, body) { + const webView = this.getWebView(body); + + if (!webView) { + this.queue.push({type, body}); + return; + } + + this._send(webView, type, body); + } + + _send(webView, type, body) { + const {__isSandbox, ...cleanBody} = body; + + webView.injectJavaScript(` + (function(){ + (navigator.appVersion.includes("Android") ? document : window).dispatchEvent( + new MessageEvent('message', {data: ${JSON.stringify({type, body: cleanBody})}}) + ); + })(); + `); + } + + _processQueue() { + if (this.isProcessing || this.queue.length === 0) { + return; + } + + this.isProcessing = true; + + const pending = []; + + while (this.queue.length > 0) { + const {type, body} = this.queue.shift(); + + try { + const webView = this.getWebView(body); + + if (!webView) { + pending.push({type, body}); + continue; + } + + this._send(webView, type, body); + } catch (err) { + console.warn('Failed to process queued message, discarding:', err.message); + } + } + + if (pending.length > 0) { + this.queue = pending; + } + + this.isProcessing = false; + } + + _startProcessor() { + this.intervalId = setInterval(() => this._processQueue(), 5000); + } + + destroy() { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + this.queue = []; + } +} + export class WebviewEventHandler { constructor({webViewRef, sandboxWebViewRef, onReady}) { assert(!!webViewRef, 'webViewRef is required'); @@ -14,6 +93,16 @@ export class WebviewEventHandler { this.webViewRef = webViewRef; this.sandboxWebViewRef = sandboxWebViewRef; this.onReady = onReady; + + this.dispatcher = new MessageDispatcher((body) => { + const isSandbox = body?.__isSandbox; + const ref = isSandbox ? this.sandboxWebViewRef : this.webViewRef; + return ref.current; + }); + } + + destroy() { + this.dispatcher.destroy(); } getEventMapping() { @@ -48,24 +137,18 @@ export class WebviewEventHandler { } _dispatchEvent(type, body) { - const isSandboxMessage = body?.method?.indexOf('sandbox-') === 0; - const webview = isSandboxMessage - ? this.sandboxWebViewRef.current - : this.webViewRef.current; + const isSandbox = body?.method?.startsWith('sandbox-'); + + const processedBody = { + ...body, + __isSandbox: isSandbox, + }; - if (isSandboxMessage) { - body.method = body.method.replace('sandbox-', ''); + if (isSandbox) { + processedBody.method = body.method.replace('sandbox-', ''); } - webview.injectJavaScript(` - (function(){ - (navigator.appVersion.includes("Android") ? document : window).dispatchEvent(new MessageEvent('message', {data: ${JSON.stringify( - { - type: type, - body: body, - }, - )}})); - })();`); + this.dispatcher.dispatch(type, processedBody); } _handleRpcReady() { diff --git a/packages/react-native/lib/message-handler.test.js b/packages/react-native/lib/message-handler.test.js index 20100aa7..c06a9173 100644 --- a/packages/react-native/lib/message-handler.test.js +++ b/packages/react-native/lib/message-handler.test.js @@ -1,5 +1,5 @@ import {getRpcClient} from '@docknetwork/wallet-sdk-wasm/src/rpc-client'; -import {WebviewEventHandler} from './message-handler'; +import {WebviewEventHandler, MessageDispatcher} from './message-handler'; import rnRpcServer from './rn-rpc-server'; const testData = {test: true}; @@ -15,19 +15,218 @@ function createTestEvent(type, data = testData) { }; } -describe('Message handler', () => { +describe('MessageDispatcher', () => { + let mockWebView; + let getWebView; + let dispatcher; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + + mockWebView = { + injectJavaScript: jest.fn(), + }; + + getWebView = jest.fn(() => mockWebView); + dispatcher = new MessageDispatcher(getWebView); + }); + + afterEach(() => { + dispatcher.destroy(); + jest.useRealTimers(); + }); + + describe('dispatch', () => { + it('should send message immediately when WebView is available', () => { + const type = 'test-type'; + const body = {data: 'test-data'}; + + dispatcher.dispatch(type, body); + + expect(getWebView).toHaveBeenCalledWith(body); + expect(mockWebView.injectJavaScript).toHaveBeenCalledTimes(1); + expect(mockWebView.injectJavaScript.mock.calls[0][0]).toContain( + JSON.stringify({type, body}) + ); + }); + + it('should queue message when WebView is unavailable', () => { + getWebView.mockReturnValue(null); + + const type = 'test-type'; + const body = {data: 'test-data'}; + + dispatcher.dispatch(type, body); + + expect(mockWebView.injectJavaScript).not.toHaveBeenCalled(); + expect(dispatcher.queue).toHaveLength(1); + expect(dispatcher.queue[0]).toEqual({type, body}); + }); + + it('should throw if getWebView throws', () => { + getWebView.mockImplementation(() => { + throw new Error('unexpected error'); + }); + + expect(() => dispatcher.dispatch('type', {data: 'test'})).toThrow('unexpected error'); + expect(dispatcher.queue).toHaveLength(0); + }); + }); + + describe('queue processing', () => { + it('should process queued messages when WebView becomes available', () => { + getWebView.mockReturnValue(null); + + // Queue some messages + dispatcher.dispatch('type1', {data: 'message1'}); + dispatcher.dispatch('type2', {data: 'message2'}); + + expect(dispatcher.queue).toHaveLength(2); + + // Make WebView available + getWebView.mockReturnValue(mockWebView); + + // Fast-forward time to trigger queue processing + jest.advanceTimersByTime(5000); + + expect(mockWebView.injectJavaScript).toHaveBeenCalledTimes(2); + expect(dispatcher.queue).toHaveLength(0); + }); + + it('should keep messages queued if WebView is still unavailable', () => { + getWebView.mockReturnValue(null); + + dispatcher.dispatch('type1', {data: 'message1'}); + dispatcher.dispatch('type2', {data: 'message2'}); + + expect(dispatcher.queue).toHaveLength(2); + + // Trigger queue processing but WebView still unavailable + jest.advanceTimersByTime(5000); + + expect(mockWebView.injectJavaScript).not.toHaveBeenCalled(); + expect(dispatcher.queue).toHaveLength(2); + }); + + it('should handle WebView becoming unavailable mid-processing', () => { + getWebView.mockReturnValue(null); + + // Queue messages while WebView is unavailable + dispatcher.dispatch('type1', {data: 'message1'}); + dispatcher.dispatch('type2', {data: 'message2'}); + dispatcher.dispatch('type3', {data: 'message3'}); + expect(dispatcher.queue).toHaveLength(3); + + // Now set up WebView to become unavailable after first message + let callCount = 0; + getWebView.mockImplementation(() => { + callCount++; + return callCount <= 1 ? mockWebView : null; + }); + + jest.advanceTimersByTime(5000); + + // First message sent, remaining re-queued + expect(mockWebView.injectJavaScript).toHaveBeenCalledTimes(1); + expect(dispatcher.queue).toHaveLength(2); + }); + + it('should discard message and continue processing if send throws', () => { + getWebView.mockReturnValue(null); + + dispatcher.dispatch('type1', {data: 'message1'}); + dispatcher.dispatch('type2', {data: 'message2'}); + expect(dispatcher.queue).toHaveLength(2); + + // First message throws, second succeeds + let callCount = 0; + getWebView.mockReturnValue(mockWebView); + mockWebView.injectJavaScript.mockImplementation(() => { + callCount++; + if (callCount === 1) { + throw new Error('WebView crashed'); + } + }); + + const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + jest.advanceTimersByTime(5000); + + // First message discarded, second sent successfully + expect(mockWebView.injectJavaScript).toHaveBeenCalledTimes(2); + expect(dispatcher.queue).toHaveLength(0); + expect(consoleWarn).toHaveBeenCalledWith( + 'Failed to process queued message, discarding:', + 'WebView crashed' + ); + + consoleWarn.mockRestore(); + }); + + it('should not process queue concurrently', () => { + getWebView.mockReturnValue(null); + + dispatcher.dispatch('type1', {data: 'message1'}); + + dispatcher.isProcessing = true; + + jest.advanceTimersByTime(5000); + + // Should not process because isProcessing is true + expect(dispatcher.queue).toHaveLength(1); + }); + }); + + describe('destroy', () => { + it('should clear interval and queue', () => { + const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); + + // Queue a message when WebView is unavailable + getWebView.mockReturnValue(null); + + dispatcher.dispatch('type1', {data: 'message1'}); + expect(dispatcher.queue).toHaveLength(1); + + dispatcher.destroy(); + + expect(clearIntervalSpy).toHaveBeenCalled(); + expect(dispatcher.intervalId).toBeNull(); + expect(dispatcher.queue).toHaveLength(0); + }); + }); +}); + +describe('WebviewEventHandler', () => { const webViewRef = { current: { injectJavaScript: jest.fn(), }, }; + const sandboxWebViewRef = { + current: { + injectJavaScript: jest.fn(), + }, + }; const onReady = jest.fn(); let eventHandler: WebviewEventHandler; + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + }); + + afterEach(() => { + if (eventHandler) { + eventHandler.destroy(); + } + jest.useRealTimers(); + }); + beforeAll(() => { eventHandler = new WebviewEventHandler({ onReady, webViewRef, + sandboxWebViewRef, }); jest.spyOn(eventHandler, '_handleRpcResponse'); @@ -44,7 +243,9 @@ describe('Message handler', () => { it('expect to create message handler', () => { expect(eventHandler.webViewRef).toBe(webViewRef); + expect(eventHandler.sandboxWebViewRef).toBe(sandboxWebViewRef); expect(eventHandler.onReady).toBe(onReady); + expect(eventHandler.dispatcher).toBeDefined(); }); it('expect to handle json-rpc-ready event', async () => { @@ -80,4 +281,89 @@ describe('Message handler', () => { await eventHandler._dispatchEvent('test', body); expect(webViewRef.current.injectJavaScript).toBeCalled(); }); + + it('should use sandbox webview for sandbox messages', () => { + const freshWebViewRef = { + current: { + injectJavaScript: jest.fn(), + }, + }; + const freshSandboxWebViewRef = { + current: { + injectJavaScript: jest.fn(), + }, + }; + + const handler = new WebviewEventHandler({ + onReady: jest.fn(), + webViewRef: freshWebViewRef, + sandboxWebViewRef: freshSandboxWebViewRef, + }); + + const body = {method: 'sandbox-testMethod', data: 'test'}; + + handler._dispatchEvent('test', body); + + expect(freshSandboxWebViewRef.current.injectJavaScript).toHaveBeenCalled(); + expect(freshWebViewRef.current.injectJavaScript).not.toHaveBeenCalled(); + + handler.destroy(); + }); + + it('should strip sandbox- prefix from method name', () => { + const freshWebViewRef = { + current: { + injectJavaScript: jest.fn(), + }, + }; + const freshSandboxWebViewRef = { + current: { + injectJavaScript: jest.fn(), + }, + }; + + const handler = new WebviewEventHandler({ + onReady: jest.fn(), + webViewRef: freshWebViewRef, + sandboxWebViewRef: freshSandboxWebViewRef, + }); + + const body = {method: 'sandbox-testMethod', data: 'test'}; + + handler._dispatchEvent('test', body); + + const injectedScript = freshSandboxWebViewRef.current.injectJavaScript.mock.calls[0][0]; + expect(injectedScript).toContain('"method":"testMethod"'); + expect(injectedScript).not.toContain('sandbox-'); + + handler.destroy(); + }); + + it('should queue messages when webview is unavailable', () => { + const handler = new WebviewEventHandler({ + onReady: jest.fn(), + webViewRef: {current: null}, + sandboxWebViewRef: {current: null}, + }); + + handler._dispatchEvent('test', {data: 'test'}); + + expect(handler.dispatcher.queue).toHaveLength(1); + + handler.destroy(); + }); + + it('should cleanup on destroy', () => { + const handler = new WebviewEventHandler({ + onReady: jest.fn(), + webViewRef, + sandboxWebViewRef, + }); + + const destroySpy = jest.spyOn(handler.dispatcher, 'destroy'); + + handler.destroy(); + + expect(destroySpy).toHaveBeenCalled(); + }); }); diff --git a/setup-integration-tests.js b/setup-integration-tests.js index adb19ef9..0fab784b 100644 --- a/setup-integration-tests.js +++ b/setup-integration-tests.js @@ -7,6 +7,16 @@ NetworkManager.getInstance().setNetworkId('testnet'); process.env.ENCRYPTION_KEY = '776fe87eec8c9ba8417beda00b23cf22f5e134d9644d0a195cd9e0b7373760c1'; +jest.retryTimes(2, {logErrorsBeforeRetry: true}); + +const testAttempts = {}; +beforeEach(async () => { + const testName = expect.getState().currentTestName; + testAttempts[testName] = (testAttempts[testName] || 0) + 1; + if (testAttempts[testName] > 1) { + await new Promise(resolve => setTimeout(resolve, 5000)); + } +}); jest.mock('@react-native-async-storage/async-storage', () => 'AsyncStorage');