diff --git a/services/data/src/react/hooks/useDataQuery.ts b/services/data/src/react/hooks/useDataQuery.ts index 6e4d3862..a7d16fb2 100644 --- a/services/data/src/react/hooks/useDataQuery.ts +++ b/services/data/src/react/hooks/useDataQuery.ts @@ -1,11 +1,10 @@ import { useState, useRef, useCallback, useDebugValue } from 'react' import { useQuery, setLogger } from 'react-query' -import { - JsonMap, - type Query, - type QueryOptions, - type QueryResult, - type QueryVariables, +import type { + Query, + QueryOptions, + QueryResult, + QueryVariables, } from '../../engine' import type { FetchError } from '../../engine/types/FetchError' import type { QueryRenderInput, QueryRefetchFunction } from '../../types' diff --git a/services/offline/src/__tests__/integration.test.tsx b/services/offline/src/__tests__/integration.test.tsx index db760d77..0d92427c 100644 --- a/services/offline/src/__tests__/integration.test.tsx +++ b/services/offline/src/__tests__/integration.test.tsx @@ -43,7 +43,7 @@ const TestControls = ({ />
{isCached ? 'yes' : 'no'}
- {lastUpdated || 'never'} + {lastUpdated?.toISOString() ?? 'never'}
{recordingState}
@@ -112,7 +112,7 @@ describe('Coordination between useCacheableSection and CacheableSection', () => expect(getByTestId(/controls-rc/)).toBeInTheDocument() }) - it.skip('handles a successful recording', async (done) => { + it('handles a successful recording', async (done) => { const { getByTestId, queryByTestId } = screen const onStarted = () => { @@ -147,7 +147,7 @@ describe('Coordination between useCacheableSection and CacheableSection', () => expect.assertions(7) }) - it.skip('handles a recording that encounters an error', async (done) => { + it('handles a recording that encounters an error', async (done) => { // Suppress the expected error from console (in addition to 'act' warning) jest.spyOn(console, 'error').mockImplementation((...args) => { const actPattern = @@ -255,7 +255,7 @@ describe('Performant state management', () => { expect(getByTestId('section-rc-2')).toHaveTextContent('1') }) - it.skip('isolates rerenders from other consumers', async (done) => { + it('isolates rerenders from other consumers', async (done) => { const { getByTestId } = screen // Make assertions const onCompleted = () => { @@ -300,12 +300,12 @@ describe('useCacheableSection can be used inside a child of CacheableSection', ( ) } - it.skip('handles a successful recording', async (done) => { - const { getByTestId, queryByTestId, findByTestId } = screen + it('handles a successful recording', async (done) => { + const { getByTestId, queryByTestId } = screen const onStarted = async () => { await waitFor(() => { - expect(getByTestId(/recording-state/)).toBeInTheDocument( + expect(getByTestId(/recording-state/)).toHaveTextContent( 'recording' ) expect(getByTestId(/loading-mask/)).toBeInTheDocument() @@ -326,9 +326,9 @@ describe('useCacheableSection can be used inside a child of CacheableSection', ( render() - // await act(async () => { - await fireEvent.click(getByTestId(/start-recording/)) - // }) + await act(async () => { + await fireEvent.click(getByTestId(/start-recording/)) + }) await waitFor(() => { // At this stage, should be pending diff --git a/services/offline/src/lib/__tests__/cacheable-section-state.test.tsx b/services/offline/src/lib/__tests__/cacheable-section-state.test.tsx index 8910c70f..ff897ca2 100644 --- a/services/offline/src/lib/__tests__/cacheable-section-state.test.tsx +++ b/services/offline/src/lib/__tests__/cacheable-section-state.test.tsx @@ -1,10 +1,10 @@ -import { renderHook } from '@testing-library/react-hooks' -import React, { FC } from 'react' +import { renderHook } from '@testing-library/react' +import React, { FC, PropsWithChildren } from 'react' import { mockOfflineInterface } from '../../utils/test-mocks' import { useCachedSection, useRecordingState } from '../cacheable-section-state' import { OfflineProvider } from '../offline-provider' -const wrapper: FC = ({ children }) => ( +const wrapper: FC = ({ children }) => ( {children} diff --git a/services/offline/src/lib/__tests__/use-cacheable-section.test.tsx b/services/offline/src/lib/__tests__/use-cacheable-section.test.tsx index cfc2cc1b..8e8743a6 100644 --- a/services/offline/src/lib/__tests__/use-cacheable-section.test.tsx +++ b/services/offline/src/lib/__tests__/use-cacheable-section.test.tsx @@ -27,7 +27,7 @@ afterEach(() => { ;(console.error as jest.Mock).mockRestore() }) -it.skip('renders in the default state initially', () => { +it('renders in the default state initially', () => { const wrapper: FC = ({ children }) => ( {children} @@ -42,7 +42,7 @@ it.skip('renders in the default state initially', () => { }) it('has stable references', () => { - const wrapper: FC = ({ children }) => ( + const wrapper: FC = ({ children }) => ( {children} @@ -66,9 +66,9 @@ it('has stable references', () => { expect(result.current.remove).toBe(origRemove) }) -it.skip('handles a successful recording', async (done) => { +it('handles a successful recording', async (done) => { const [sectionId, timeoutDelay] = ['one', 1234] - const testOfflineInterface = { + const recordingSuccessOfflineInterface = { ...mockOfflineInterface, getCachedSections: jest .fn() @@ -78,7 +78,7 @@ it.skip('handles a successful recording', async (done) => { ]), } const wrapper: FC = ({ children }) => ( - + {children} ) @@ -93,8 +93,16 @@ it.skip('handles a successful recording', async (done) => { expect(result.current.recordingState).toBe('default') // Test that 'isCached' gets updated - expect(testOfflineInterface.getCachedSections).toBeCalledTimes(2) - await waitFor(() => expect(result.current.isCached).toBe(true)) + expect( + recordingSuccessOfflineInterface.getCachedSections + ).toBeCalledTimes(2) + // Recording states are updated synchronously, but getting isCached + // state is asynchronous -- need to wait for that here. + // An assertion is not used as the waitFor condition because it may skew + // the total number assertions in this test if it needs to retry. Number + // of assertions is checked at the bottom of this test to make sure both + // of these callbacks are called. + await waitFor(() => result.current.isCached === true) expect(result.current.isCached).toBe(true) expect(result.current.lastUpdated).toBeInstanceOf(Date) @@ -125,7 +133,7 @@ it.skip('handles a successful recording', async (done) => { expect.assertions(11) }) -it.skip('handles a recording that encounters an error', async (done) => { +it('handles a recording that encounters an error', async (done) => { // Suppress the expected error from console (in addition to 'act' warning) jest.spyOn(console, 'error').mockImplementation((...args) => { const actPattern = @@ -138,12 +146,12 @@ it.skip('handles a recording that encounters an error', async (done) => { } return originalError.call(console, ...args) }) - const testOfflineInterface = { + const recordingErrorOfflineInterface = { ...mockOfflineInterface, startRecording: errorRecordingMock, } const wrapper: FC = ({ children }) => ( - + {children} ) @@ -181,13 +189,13 @@ it.skip('handles a recording that encounters an error', async (done) => { expect.assertions(6) }) -it.skip('handles an error starting the recording', async () => { - const testOfflineInterface = { +it('handles an error starting the recording', async () => { + const messageErrorOfflineInterface = { ...mockOfflineInterface, startRecording: failedMessageRecordingMock, } const wrapper: FC = ({ children }) => ( - + {children} ) @@ -198,9 +206,9 @@ it.skip('handles an error starting the recording', async () => { ) }) -it.skip('handles remove and updates sections', async () => { +it('handles remove and updates sections', async () => { const sectionId = 'one' - const testOfflineInterface = { + const sectionOpsOfflineInterface = { ...mockOfflineInterface, getCachedSections: jest .fn() @@ -210,7 +218,7 @@ it.skip('handles remove and updates sections', async () => { .mockResolvedValueOnce([]), } const wrapper: FC = ({ children }) => ( - + {children} ) @@ -228,14 +236,14 @@ it.skip('handles remove and updates sections', async () => { expect(success).toBe(true) // Test that 'isCached' gets updated - expect(testOfflineInterface.getCachedSections).toBeCalledTimes(2) + expect(sectionOpsOfflineInterface.getCachedSections).toBeCalledTimes(2) await waitFor(() => expect(result.current.isCached).toBe(false)) expect(result.current.isCached).toBe(false) expect(result.current.lastUpdated).toBeUndefined() }) -it.skip('handles a change in ID', async () => { - const testOfflineInterface = { +it('handles a change in ID', async () => { + const idChangeOfflineInterface = { ...mockOfflineInterface, getCachedSections: jest .fn() @@ -244,7 +252,7 @@ it.skip('handles a change in ID', async () => { ]), } const wrapper: FC = ({ children }) => ( - + {children} ) @@ -259,7 +267,7 @@ it.skip('handles a change in ID', async () => { rerender('id-two') // Test that 'isCached' gets updated - // expect(testOfflineInterface.getCachedSections).toBeCalledTimes(2) + // expect(idChangeOfflineInterface.getCachedSections).toBeCalledTimes(2) await waitFor(() => expect(result.current.isCached).toBe(false)) expect(result.current.isCached).toBe(false) expect(result.current.lastUpdated).toBeUndefined() diff --git a/services/offline/src/lib/cacheable-section-state.tsx b/services/offline/src/lib/cacheable-section-state.tsx index 27b9aae9..67716615 100644 --- a/services/offline/src/lib/cacheable-section-state.tsx +++ b/services/offline/src/lib/cacheable-section-state.tsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types' import React, { useEffect, useCallback, useMemo } from 'react' import { GlobalStateStore, + GlobalStateStoreMutationCreator, IndexedDBCachedSection, RecordingState, } from '../types' @@ -108,12 +109,14 @@ interface RecordingStateControls { */ export function useRecordingState(id: string): RecordingStateControls { const recordingStateSelector = useCallback( - (state) => state.recordingStates[id], + (state: any) => state.recordingStates[id], [id] ) const [recordingState] = useGlobalState(recordingStateSelector) - const setRecordingStateMutationCreator = useCallback( + const setRecordingStateMutationCreator = useCallback< + GlobalStateStoreMutationCreator + >( (newState) => (state: any) => ({ ...state, recordingStates: { ...state.recordingStates, [id]: newState }, @@ -155,7 +158,9 @@ export function useRecordingState(id: string): RecordingStateControls { function useSyncCachedSections() { const offlineInterface = useOfflineInterface() - const setCachedSectionsMutationCreator = useCallback( + const setCachedSectionsMutationCreator = useCallback< + GlobalStateStoreMutationCreator + >( (cachedSections) => (state: any) => ({ ...state, cachedSections, diff --git a/services/offline/src/lib/cacheable-section.tsx b/services/offline/src/lib/cacheable-section.tsx index 98b1fcdd..57484d52 100644 --- a/services/offline/src/lib/cacheable-section.tsx +++ b/services/offline/src/lib/cacheable-section.tsx @@ -1,5 +1,6 @@ import PropTypes from 'prop-types' import React, { useCallback, useEffect, useMemo } from 'react' +import { flushSync } from 'react-dom' import { RecordingState } from '../types' import { useRecordingState, useCachedSection } from './cacheable-section-state' import { useOfflineInterface } from './offline-interface' @@ -95,15 +96,23 @@ export function useCacheableSection(id: string): CacheableSectionControls { sectionId: id, recordingTimeoutDelay, onStarted: () => { - onRecordingStarted() + // Flush this state update synchronously so that the + // right recordingState is set before any other callbacks + flushSync(() => { + onRecordingStarted() + }) onStarted && onStarted() }, onCompleted: () => { - onRecordingCompleted() + flushSync(() => { + onRecordingCompleted() + }) onCompleted && onCompleted() }, onError: (error) => { - onRecordingError(error) + flushSync(() => { + onRecordingError(error) + }) onError && onError(error) }, })