import JoltClient from '@planningcenter/jolt-client' import { JoltSubscription } from '@planningcenter/jolt-client/dist/types/JoltSubscription' import { QueryClientProvider } from '@tanstack/react-query' import { render, waitFor } from '@testing-library/react-native' import React, { PropsWithChildren, Suspense } from 'react' import { AppState, AppStateStatus } from 'react-native' import { buildTestQueryClient } from '../../__utils__/query_client' import { ChatContext, ChatContextValue } from '../../contexts/chat_context' import { Session } from '../../utils' import { Client } from '../../utils/client' import { ResponseError } from '../../utils/response_error' import { useJoltClient, useJoltForegroundPing } from '../use_jolt' let caught: unknown class Catch extends React.Component { state = { failed: false } static getDerivedStateFromError(error: unknown) { caught = error return { failed: true } } render() { return this.state.failed ? null : this.props.children } } const Probe = () => { useJoltClient() return null } const renderJoltClient = () => { const session = new Session({ token: { access_token: 'a-real-token' } }) return render( ) } describe('useJoltClient', () => { let post: jest.SpyInstance let consoleError: jest.SpyInstance beforeEach(() => { caught = undefined post = jest.spyOn(Client.prototype, 'post') consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}) }) afterEach(() => { post.mockRestore() consoleError.mockRestore() }) it('surfaces a failed jolt_authorize as a ResponseError', async () => { post.mockRejectedValue(new Response(null, { status: 401, statusText: 'Unauthorized' })) renderJoltClient() await waitFor(() => expect(caught).toBeInstanceOf(ResponseError)) expect((caught as ResponseError).status).toBe(401) }) }) describe('useJoltForegroundPing', () => { const addEventListener = AppState.addEventListener as jest.Mock const confirmedSubscription = {} as JoltSubscription const PingProbe = () => { useJoltForegroundPing() return null } const renderForegroundPing = (joltClient: Partial, { mounts = 1 } = {}) => { const queryClient = buildTestQueryClient({ defaultOptions: { queries: { gcTime: Infinity } } }) queryClient.setQueryData(['jolt-client', 'wss://jolt.test', 'a-real-token'], joltClient) const result = render( {Array.from({ length: mounts }, (_, index) => ( ))} ) const [, onChange] = addEventListener.mock.calls.at(-1) as [ string, (_state: AppStateStatus) => void, ] return { onChange, unmount: result.unmount } } beforeEach(() => { addEventListener.mockClear() }) it('pings the jolt client once when the app returns to the foreground', () => { const ping = jest.fn() const { onChange } = renderForegroundPing({ ping, subscriptions: [confirmedSubscription] }) onChange('background') onChange('active') expect(ping).toHaveBeenCalledTimes(1) }) it('registers one listener and pings once no matter how many providers mount it', () => { const ping = jest.fn() const remove = jest.fn() addEventListener.mockReturnValueOnce({ remove }) const { onChange, unmount } = renderForegroundPing( { ping, subscriptions: [confirmedSubscription] }, { mounts: 3 } ) onChange('background') onChange('active') expect(addEventListener).toHaveBeenCalledTimes(1) expect(ping).toHaveBeenCalledTimes(1) unmount() expect(remove).toHaveBeenCalledTimes(1) }) it('does not ping when there are no confirmed subscriptions', () => { const ping = jest.fn() const { onChange } = renderForegroundPing({ ping, subscriptions: [] }) onChange('background') onChange('active') expect(ping).not.toHaveBeenCalled() }) it('does not ping when the app leaves the foreground', () => { const ping = jest.fn() const { onChange } = renderForegroundPing({ ping, subscriptions: [confirmedSubscription] }) onChange('inactive') onChange('background') expect(ping).not.toHaveBeenCalled() }) it('swallows errors from pinging a closed socket', () => { const ping = jest.fn(() => { throw new Error('No WebSocket') }) const { onChange } = renderForegroundPing({ ping, subscriptions: [confirmedSubscription] }) onChange('background') expect(() => onChange('active')).not.toThrow() expect(ping).toHaveBeenCalledTimes(1) }) })