diff --git a/test/sdk.test.ts b/test/sdk.test.ts index 201a192762..00a3d893dd 100644 --- a/test/sdk.test.ts +++ b/test/sdk.test.ts @@ -1,88 +1,23 @@ -/** - * @jest-environment jsdom - */ -import { logger } from '@sentry/utils'; - -interface MockedClient { - flush: jest.Mock; -} - -let mockedGetCurrentHubWithScope: jest.Mock; -let mockedGetCurrentHubConfigureScope: jest.Mock; - -jest.mock('@sentry/react', () => { - const actualModule = jest.requireActual('@sentry/react'); - - const mockClient: MockedClient = { - flush: jest.fn(() => Promise.resolve(true)), - }; - - return { - ...actualModule, - getCurrentHub: jest.fn(() => { - mockedGetCurrentHubWithScope = jest.fn(); - mockedGetCurrentHubConfigureScope = jest.fn(); - return { - getClient: jest.fn(() => mockClient), - setTag: jest.fn(), - withScope: mockedGetCurrentHubWithScope, - configureScope: mockedGetCurrentHubConfigureScope, - }; - }), - }; -}); - -jest.mock('@sentry/core', () => { - const originalCore = jest.requireActual('@sentry/core'); - return { - ...originalCore, - initAndBind: jest.fn(), - }; -}); - -jest.mock('@sentry/hub', () => { - const originalHub = jest.requireActual('@sentry/hub'); - return { - ...originalHub, - makeMain: jest.fn(), - }; -}); - -jest.mock('../src/js/scope', () => { - return { - ReactNativeScope: class ReactNativeScopeMock {}, - }; -}); - -jest.mock('../src/js/client', () => { - return { - ReactNativeClient: class ReactNativeClientMock {}, - }; -}); - -import * as mockedWrapper from './mockWrapper'; -jest.mock('../src/js/wrapper', () => mockedWrapper); -jest.mock('../src/js/utils/environment'); - jest.spyOn(logger, 'error'); +jest.mock('../src/js/wrapper', () => jest.requireActual('./mockWrapper')); +jest.mock('../src/js/utils/environment'); +jest.mock('@sentry/core', () => ({ + ...jest.requireActual('@sentry/core'), + initAndBind: jest.fn(), +})); import { initAndBind } from '@sentry/core'; -import { getCurrentHub, makeFetchTransport } from '@sentry/react'; +import { makeFetchTransport } from '@sentry/react'; import type { BaseTransportOptions, ClientOptions, Integration, Scope } from '@sentry/types'; +import { logger } from '@sentry/utils'; -import type { ReactNativeClientOptions } from '../src/js/options'; -import { configureScope, flush, init, withScope } from '../src/js/sdk'; +import { init, withScope } from '../src/js/sdk'; import { ReactNativeTracing, ReactNavigationInstrumentation } from '../src/js/tracing'; import { makeNativeTransport } from '../src/js/transports/native'; import { getDefaultEnvironment, isExpoGo, notWeb } from '../src/js/utils/environment'; import { NATIVE } from './mockWrapper'; import { firstArg, secondArg } from './testutils'; -const mockedInitAndBind = initAndBind as jest.MockedFunction; -const usedOptions = (): ClientOptions | undefined => { - return mockedInitAndBind.mock.calls[0]?.[1]; -}; - describe('Tests the SDK functionality', () => { beforeEach(() => { (NATIVE.isNativeAvailable as jest.Mock).mockImplementation(() => true); @@ -95,21 +30,6 @@ describe('Tests the SDK functionality', () => { describe('init', () => { describe('enableAutoPerformanceTracing', () => { - const usedOptions = (): Integration[] => { - const mockCall = mockedInitAndBind.mock.calls[0]; - - if (mockCall) { - const options = mockCall[1]; - - return options.integrations; - } - return []; - }; - - const autoPerformanceIsEnabled = (): boolean => { - return usedOptions().some(integration => integration.name === ReactNativeTracing.id); - }; - const reactNavigationInstrumentation = (): ReactNativeTracing => { const nav = new ReactNavigationInstrumentation(); return new ReactNativeTracing({ routingInstrumentation: nav }); @@ -163,7 +83,7 @@ describe('Tests the SDK functionality', () => { integrations: [tracing], }); - const options = usedOptions(); + const options = usedIntegrations(); expect(options.filter(integration => integration.name === ReactNativeTracing.id).length).toBe(1); expect(options.some(integration => integration === tracing)).toBe(true); }); @@ -176,42 +96,12 @@ describe('Tests the SDK functionality', () => { defaultIntegrations: [tracing], }); - const options = usedOptions(); + const options = usedIntegrations(); expect(options.filter(integration => integration.name === ReactNativeTracing.id).length).toBe(1); expect(options.some(integration => integration === tracing)).toBe(true); }); }); - describe('flush', () => { - it('Calls flush on the client', async () => { - const mockClient = getMockClient(); - - expect(mockClient).toBeTruthy(); - - if (mockClient) { - const flushResult = await flush(); - - expect(mockClient.flush).toBeCalled(); - expect(flushResult).toBe(true); - } - }); - - it('Returns false if flush failed and logs error', async () => { - const mockClient = getMockClient(); - - expect(mockClient).toBeTruthy(); - if (mockClient) { - mockClient.flush = jest.fn(() => Promise.reject()); - - const flushResult = await flush(); - - expect(mockClient.flush).toBeCalled(); - expect(flushResult).toBe(false); - expect(logger.error).toBeCalledWith('Failed to flush the event queue.'); - } - }); - }); - describe('environment', () => { it('detect development environment', () => { (getDefaultEnvironment as jest.Mock).mockImplementation(() => 'development'); @@ -356,7 +246,7 @@ describe('Tests the SDK functionality', () => { init({ initialScope: mockInitialScope }); expect(() => { - (mockedInitAndBind.mock.calls[0][secondArg].initialScope as (scope: Scope) => Scope)({} as any); + (usedOptions()?.initialScope as (scope: Scope) => Scope)({} as any); }).not.toThrow(); expect(mockInitialScope).toBeCalledTimes(1); }); @@ -368,7 +258,7 @@ describe('Tests the SDK functionality', () => { init({ beforeBreadcrumb: mockBeforeBreadcrumb }); expect(() => { - mockedInitAndBind.mock.calls[0][secondArg].beforeBreadcrumb?.({} as any); + usedOptions()?.beforeBreadcrumb?.({} as any); }).not.toThrow(); expect(mockBeforeBreadcrumb).toBeCalledTimes(1); }); @@ -392,7 +282,7 @@ describe('Tests the SDK functionality', () => { init({ tracesSampler: mockTraceSampler }); expect(() => { - mockedInitAndBind.mock.calls[0][secondArg].tracesSampler?.({} as any); + usedOptions()?.tracesSampler?.({} as any); }).not.toThrow(); expect(mockTraceSampler).toBeCalledTimes(1); }); @@ -404,39 +294,20 @@ describe('Tests the SDK functionality', () => { throw 'Test error'; }); - withScope(mockScopeCallback); - - expect(() => { - (mockedGetCurrentHubWithScope.mock.calls[0][firstArg] as (scope: Scope) => void)({} as any); - }).not.toThrow(); - expect(mockScopeCallback).toBeCalledTimes(1); - }); - }); - - describe('configureScope', () => { - test('configureScope callback does not throw', () => { - const mockScopeCallback = jest.fn(() => { - throw 'Test error'; - }); - - configureScope(mockScopeCallback); - - expect(() => { - (mockedGetCurrentHubConfigureScope.mock.calls[0][firstArg] as (scope: Scope) => void)({} as any); - }).not.toThrow(); + expect(() => withScope(mockScopeCallback)).not.toThrow(); expect(mockScopeCallback).toBeCalledTimes(1); }); }); describe('integrations', () => { it('replaces default integrations', () => { - const mockDefaultIntegration = getMockedIntegration(); + const mockDefaultIntegration = createMockedIntegration(); init({ defaultIntegrations: [mockDefaultIntegration], }); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual([mockDefaultIntegration]); }); @@ -444,8 +315,8 @@ describe('Tests the SDK functionality', () => { it('no http client integration by default', () => { init({}); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'HttpClient' })])); }); @@ -455,8 +326,8 @@ describe('Tests the SDK functionality', () => { enableCaptureFailedRequests: true, }); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'HttpClient' })])); }); @@ -473,8 +344,8 @@ describe('Tests the SDK functionality', () => { ], }); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual( expect.arrayContaining([ @@ -484,14 +355,14 @@ describe('Tests the SDK functionality', () => { }), ]), ); - expect(actualIntegrations.filter(integration => integration.name === 'HttpClient')).toHaveLength(1); + expect(actualIntegrations?.filter(integration => integration.name === 'HttpClient')).toHaveLength(1); }); it('no screenshot integration by default', () => { init({}); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'Screenshot' })])); }); @@ -501,8 +372,8 @@ describe('Tests the SDK functionality', () => { attachScreenshot: true, }); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'Screenshot' })])); }); @@ -510,8 +381,8 @@ describe('Tests the SDK functionality', () => { it('no view hierarchy integration by default', () => { init({}); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual( expect.not.arrayContaining([expect.objectContaining({ name: 'ViewHierarchy' })]), @@ -523,8 +394,8 @@ describe('Tests the SDK functionality', () => { attachViewHierarchy: true, }); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'ViewHierarchy' })])); }); @@ -532,8 +403,8 @@ describe('Tests the SDK functionality', () => { it('no profiling integration by default', () => { init({}); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual( expect.not.arrayContaining([expect.objectContaining({ name: 'HermesProfiling' })]), ); @@ -546,8 +417,8 @@ describe('Tests the SDK functionality', () => { }, }); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual( expect.arrayContaining([expect.objectContaining({ name: 'HermesProfiling' })]), ); @@ -556,8 +427,8 @@ describe('Tests the SDK functionality', () => { it('no spotlight integration by default', () => { init({}); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'Spotlight' })])); }); @@ -566,8 +437,8 @@ describe('Tests the SDK functionality', () => { enableSpotlight: true, }); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'Spotlight' })])); }); @@ -576,42 +447,42 @@ describe('Tests the SDK functionality', () => { defaultIntegrations: false, }); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual([]); }); it('merges with passed default integrations', () => { - const mockIntegration = getMockedIntegration(); - const mockDefaultIntegration = getMockedIntegration({ name: 'MockedDefaultIntegration' }); + const mockIntegration = createMockedIntegration(); + const mockDefaultIntegration = createMockedIntegration({ name: 'MockedDefaultIntegration' }); init({ integrations: [mockIntegration], defaultIntegrations: [mockDefaultIntegration], }); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual(expect.arrayContaining([mockIntegration, mockDefaultIntegration])); // order doesn't matter - expect(actualIntegrations.length).toBe(2); // there should be no extra unexpected integrations + expect(actualIntegrations?.length).toBe(2); // there should be no extra unexpected integrations }); it('merges with default integrations', () => { - const mockIntegration = getMockedIntegration(); + const mockIntegration = createMockedIntegration(); init({ integrations: [mockIntegration], }); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual(expect.arrayContaining([mockIntegration])); - expect(actualIntegrations.length).toBeGreaterThan(1); // there should be default integrations + the test one + expect(actualIntegrations?.length).toBeGreaterThan(1); // there should be default integrations + the test one }); it('passes default integrations to the function', () => { - const mockIntegration = getMockedIntegration(); + const mockIntegration = createMockedIntegration(); const mockIntegrationFactory = jest.fn((_integrations: Integration[]) => [mockIntegration]); init({ integrations: mockIntegrationFactory, @@ -621,15 +492,15 @@ describe('Tests the SDK functionality', () => { expect(actualPassedIntegrations.length).toBeGreaterThan(0); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual([mockIntegration]); }); it('passes custom default integrations to the function', () => { - const mockIntegration = getMockedIntegration(); - const mockDefaultIntegration = getMockedIntegration({ name: 'MockedDefaultIntegration' }); + const mockIntegration = createMockedIntegration(); + const mockDefaultIntegration = createMockedIntegration({ name: 'MockedDefaultIntegration' }); const mockIntegrationFactory = jest.fn((_integrations: Integration[]) => [mockIntegration]); init({ integrations: mockIntegrationFactory, @@ -640,8 +511,8 @@ describe('Tests the SDK functionality', () => { expect(actualPassedIntegrations).toEqual([mockDefaultIntegration]); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual([mockIntegration]); }); @@ -661,8 +532,8 @@ describe('Tests the SDK functionality', () => { it('adds react default integrations', () => { init({}); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual( expect.arrayContaining([ @@ -678,8 +549,8 @@ describe('Tests the SDK functionality', () => { it('adds all platform default integrations', () => { init({}); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual( expect.arrayContaining([ @@ -695,8 +566,8 @@ describe('Tests the SDK functionality', () => { (notWeb as jest.Mock).mockImplementation(() => false); init({}); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual( expect.arrayContaining([ @@ -717,8 +588,8 @@ describe('Tests the SDK functionality', () => { }, }); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual( expect.not.arrayContaining([expect.objectContaining({ name: 'DeviceContext' })]), @@ -740,21 +611,28 @@ describe('Tests the SDK functionality', () => { (isExpoGo as jest.Mock).mockImplementation(() => true); init({}); - const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; - const actualIntegrations = actualOptions.integrations; + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; expect(actualIntegrations).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'ExpoContext' })])); }); }); -function getMockClient(): MockedClient { - const mockClient = getCurrentHub().getClient() as unknown as MockedClient; - return mockClient; -} - -function getMockedIntegration({ name }: { name?: string } = {}): Integration { +function createMockedIntegration({ name }: { name?: string } = {}): Integration { return { name: name ?? 'MockedIntegration', setupOnce: jest.fn(), }; } + +function usedOptions(): ClientOptions | undefined { + return (initAndBind as jest.MockedFunction).mock.calls[0]?.[secondArg]; +} + +function usedIntegrations(): Integration[] { + return usedOptions()?.integrations ?? []; +} + +function autoPerformanceIsEnabled(): boolean { + return usedIntegrations().some(integration => integration.name === ReactNativeTracing.id); +} diff --git a/test/sdk.withclient.test.ts b/test/sdk.withclient.test.ts new file mode 100644 index 0000000000..654b5294e1 --- /dev/null +++ b/test/sdk.withclient.test.ts @@ -0,0 +1,49 @@ +jest.spyOn(logger, 'error'); + +import { setCurrentClient } from '@sentry/core'; +import { logger } from '@sentry/utils'; + +import { configureScope, flush } from '../src/js/sdk'; +import { getDefaultTestClientOptions, TestClient } from './mocks/client'; + +describe('Tests the SDK functionality', () => { + let client: TestClient; + + beforeEach(() => { + client = new TestClient(getDefaultTestClientOptions()); + setCurrentClient(client); + client.init(); + + jest.spyOn(client, 'flush'); + }); + + describe('flush', () => { + it('Calls flush on the client', async () => { + const flushResult = await flush(); + + expect(client.flush).toBeCalled(); + expect(flushResult).toBe(true); + }); + + it('Returns false if flush failed and logs error', async () => { + client.flush = jest.fn(() => Promise.reject()); + + const flushResult = await flush(); + + expect(client.flush).toBeCalled(); + expect(flushResult).toBe(false); + expect(logger.error).toBeCalledWith('Failed to flush the event queue.'); + }); + }); + + describe('configureScope', () => { + test('configureScope callback does not throw', () => { + const mockScopeCallback = jest.fn(() => { + throw 'Test error'; + }); + + expect(() => configureScope(mockScopeCallback)).not.toThrow(); + expect(mockScopeCallback).toBeCalledTimes(1); + }); + }); +});