Skip to content

Commit

Permalink
[libs/ui] Shared UI Strings API react-query client (#4016)
Browse files Browse the repository at this point in the history
  • Loading branch information
kofi-q authored Oct 2, 2023
1 parent 4e054a3 commit 8b41fda
Show file tree
Hide file tree
Showing 13 changed files with 346 additions and 0 deletions.
5 changes: 5 additions & 0 deletions apps/mark-scan/frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import {
AUTH_STATUS_POLLING_INTERVAL_MS,
QUERY_CLIENT_DEFAULT_OPTIONS,
createUiStringsApi,
} from '@votingworks/ui';
import { STATE_MACHINE_POLLING_INTERVAL_MS } from './constants';

Expand Down Expand Up @@ -193,6 +194,8 @@ export const endCardlessVoterSession = {
},
} as const;

export const uiStringsApi = createUiStringsApi(useApiClient);

export const configureBallotPackageFromUsb = {
useMutation() {
const apiClient = useApiClient();
Expand All @@ -201,6 +204,7 @@ export const configureBallotPackageFromUsb = {
async onSuccess() {
await queryClient.invalidateQueries(getElectionDefinition.queryKey());
await queryClient.invalidateQueries(getSystemSettings.queryKey());
await uiStringsApi.onMachineConfigurationChange(queryClient);
},
});
},
Expand All @@ -215,6 +219,7 @@ export const unconfigureMachine = {
await queryClient.invalidateQueries(getElectionDefinition.queryKey());
await queryClient.invalidateQueries(getSystemSettings.queryKey());
await queryClient.invalidateQueries(getPrecinctSelection.queryKey());
await uiStringsApi.onMachineConfigurationChange(queryClient);
},
});
},
Expand Down
73 changes: 73 additions & 0 deletions apps/mark-scan/frontend/src/api_machine_configuration.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ok } from '@votingworks/basics';
import { electionGeneralDefinition } from '@votingworks/fixtures';
import { renderHook, waitFor } from '../test/react_testing_library';
import {
ApiClient,
ApiClientContext,
configureBallotPackageFromUsb,
createApiClient,
uiStringsApi,
unconfigureMachine,
} from './api';

const queryClient = new QueryClient();
const mockBackendApi: ApiClient = {
...createApiClient(),
configureBallotPackageFromUsb: jest.fn(),
unconfigureMachine: jest.fn(),
};

function QueryWrapper(props: { children: React.ReactNode }) {
const { children } = props;

return (
<ApiClientContext.Provider value={mockBackendApi}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</ApiClientContext.Provider>
);
}

const mockOnConfigurationChange = jest.spyOn(
uiStringsApi,
'onMachineConfigurationChange'
);

afterAll(() => {
jest.restoreAllMocks();
});

test('configureBallotPackageFromUsb', async () => {
jest
.mocked(mockBackendApi)
.configureBallotPackageFromUsb.mockResolvedValueOnce(
ok(electionGeneralDefinition)
);

const { result } = renderHook(
() => configureBallotPackageFromUsb.useMutation(),
{ wrapper: QueryWrapper }
);

expect(mockOnConfigurationChange).not.toHaveBeenCalled();

result.current.mutate();
await waitFor(() => expect(result.current.isSuccess).toEqual(true));

expect(mockOnConfigurationChange).toHaveBeenCalled();
});

test('unconfigureMachine', async () => {
jest.mocked(mockBackendApi).unconfigureMachine.mockResolvedValueOnce();

const { result } = renderHook(() => unconfigureMachine.useMutation(), {
wrapper: QueryWrapper,
});

expect(mockOnConfigurationChange).not.toHaveBeenCalled();

result.current.mutate();
await waitFor(() => expect(result.current.isSuccess).toEqual(true));

expect(mockOnConfigurationChange).toHaveBeenCalled();
});
5 changes: 5 additions & 0 deletions apps/mark/frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import {
AUTH_STATUS_POLLING_INTERVAL_MS,
QUERY_CLIENT_DEFAULT_OPTIONS,
createUiStringsApi,
} from '@votingworks/ui';

export type ApiClient = grout.Client<Api>;
Expand Down Expand Up @@ -150,6 +151,8 @@ export const endCardlessVoterSession = {
},
} as const;

export const uiStringsApi = createUiStringsApi(useApiClient);

export const configureBallotPackageFromUsb = {
useMutation() {
const apiClient = useApiClient();
Expand All @@ -158,6 +161,7 @@ export const configureBallotPackageFromUsb = {
async onSuccess() {
await queryClient.invalidateQueries(getElectionDefinition.queryKey());
await queryClient.invalidateQueries(getSystemSettings.queryKey());
await uiStringsApi.onMachineConfigurationChange(queryClient);
},
});
},
Expand All @@ -171,6 +175,7 @@ export const unconfigureMachine = {
async onSuccess() {
await queryClient.invalidateQueries(getElectionDefinition.queryKey());
await queryClient.invalidateQueries(getSystemSettings.queryKey());
await uiStringsApi.onMachineConfigurationChange(queryClient);
},
});
},
Expand Down
73 changes: 73 additions & 0 deletions apps/mark/frontend/src/api_machine_configuration.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ok } from '@votingworks/basics';
import { electionGeneralDefinition } from '@votingworks/fixtures';
import { renderHook, waitFor } from '../test/react_testing_library';
import {
ApiClient,
ApiClientContext,
configureBallotPackageFromUsb,
createApiClient,
uiStringsApi,
unconfigureMachine,
} from './api';

const queryClient = new QueryClient();
const mockBackendApi: ApiClient = {
...createApiClient(),
configureBallotPackageFromUsb: jest.fn(),
unconfigureMachine: jest.fn(),
};

function QueryWrapper(props: { children: React.ReactNode }) {
const { children } = props;

return (
<ApiClientContext.Provider value={mockBackendApi}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</ApiClientContext.Provider>
);
}

const mockOnConfigurationChange = jest.spyOn(
uiStringsApi,
'onMachineConfigurationChange'
);

afterAll(() => {
jest.restoreAllMocks();
});

test('configureBallotPackageFromUsb', async () => {
jest
.mocked(mockBackendApi)
.configureBallotPackageFromUsb.mockResolvedValueOnce(
ok(electionGeneralDefinition)
);

const { result } = renderHook(
() => configureBallotPackageFromUsb.useMutation(),
{ wrapper: QueryWrapper }
);

expect(mockOnConfigurationChange).not.toHaveBeenCalled();

result.current.mutate();
await waitFor(() => expect(result.current.isSuccess).toEqual(true));

expect(mockOnConfigurationChange).toHaveBeenCalled();
});

test('unconfigureMachine', async () => {
jest.mocked(mockBackendApi).unconfigureMachine.mockResolvedValueOnce();

const { result } = renderHook(() => unconfigureMachine.useMutation(), {
wrapper: QueryWrapper,
});

expect(mockOnConfigurationChange).not.toHaveBeenCalled();

result.current.mutate();
await waitFor(() => expect(result.current.isSuccess).toEqual(true));

expect(mockOnConfigurationChange).toHaveBeenCalled();
});
5 changes: 5 additions & 0 deletions apps/scan/frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
USB_DRIVE_STATUS_POLLING_INTERVAL_MS,
QUERY_CLIENT_DEFAULT_OPTIONS,
UsbDriveStatus as LegacyUsbDriveStatus,
createUiStringsApi,
} from '@votingworks/ui';
import { typedAs } from '@votingworks/basics';
import type { UsbDriveStatus } from '@votingworks/usb-drive';
Expand Down Expand Up @@ -164,13 +165,16 @@ export const ejectUsbDrive = {
},
} as const;

export const uiStringsApi = createUiStringsApi(useApiClient);

export const configureFromBallotPackageOnUsbDrive = {
useMutation() {
const apiClient = useApiClient();
const queryClient = useQueryClient();
return useMutation(apiClient.configureFromBallotPackageOnUsbDrive, {
async onSuccess() {
await queryClient.invalidateQueries(getConfig.queryKey());
await uiStringsApi.onMachineConfigurationChange(queryClient);
},
});
},
Expand All @@ -183,6 +187,7 @@ export const unconfigureElection = {
return useMutation(apiClient.unconfigureElection, {
async onSuccess() {
await queryClient.invalidateQueries(getConfig.queryKey());
await uiStringsApi.onMachineConfigurationChange(queryClient);
},
});
},
Expand Down
71 changes: 71 additions & 0 deletions apps/scan/frontend/src/api_machine_configuration.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ok } from '@votingworks/basics';
import React from 'react';
import { renderHook, waitFor } from '../test/react_testing_library';
import {
ApiClient,
ApiClientContext,
configureFromBallotPackageOnUsbDrive,
createApiClient,
unconfigureElection,
uiStringsApi,
} from './api';

const queryClient = new QueryClient();
const mockBackendApi: ApiClient = {
...createApiClient(),
configureFromBallotPackageOnUsbDrive: jest.fn(),
unconfigureElection: jest.fn(),
};

function QueryWrapper(props: { children: React.ReactNode }) {
const { children } = props;

return (
<ApiClientContext.Provider value={mockBackendApi}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</ApiClientContext.Provider>
);
}

const mockOnConfigurationChange = jest.spyOn(
uiStringsApi,
'onMachineConfigurationChange'
);

afterAll(() => {
jest.restoreAllMocks();
});

test('configureFromBallotPackageOnUsbDrive', async () => {
jest
.mocked(mockBackendApi)
.configureFromBallotPackageOnUsbDrive.mockResolvedValueOnce(ok());

const { result } = renderHook(
() => configureFromBallotPackageOnUsbDrive.useMutation(),
{ wrapper: QueryWrapper }
);

expect(mockOnConfigurationChange).not.toHaveBeenCalled();

result.current.mutate();
await waitFor(() => expect(result.current.isSuccess).toEqual(true));

expect(mockOnConfigurationChange).toHaveBeenCalled();
});

test('unconfigureElection', async () => {
jest.mocked(mockBackendApi).unconfigureElection.mockResolvedValue();

const { result } = renderHook(() => unconfigureElection.useMutation(), {
wrapper: QueryWrapper,
});

expect(mockOnConfigurationChange).not.toHaveBeenCalled();

result.current.mutate({});
await waitFor(() => expect(result.current.isSuccess).toEqual(true));

expect(mockOnConfigurationChange).toHaveBeenCalled();
});
2 changes: 2 additions & 0 deletions libs/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@testing-library/react": "^14.0.0",
"@votingworks/ballot-encoder": "workspace:*",
"@votingworks/basics": "workspace:*",
"@votingworks/grout": "workspace:*",
"@votingworks/logging": "workspace:*",
"@votingworks/types": "workspace:*",
"@votingworks/utils": "workspace:*",
Expand Down Expand Up @@ -119,6 +120,7 @@
"vite": "^4.0.4"
},
"peerDependencies": {
"@tanstack/react-query": "^4.32.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"styled-components": "^5.3.11"
Expand Down
60 changes: 60 additions & 0 deletions libs/ui/src/hooks/ui_strings_api.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { LanguageCode } from '@votingworks/types';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { renderHook, waitFor } from '../../test/react_testing_library';
import { createUiStringsApi } from './ui_strings_api';

const queryClient = new QueryClient();
function QueryWrapper(props: { children: React.ReactNode }) {
const { children } = props;

return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}

const mockApiClient = {
getAudioClipsBase64: jest.fn(),
getAvailableLanguages: jest.fn(),
getUiStringAudioIds: jest.fn(),
getUiStrings: jest.fn(),
} as const;

const api = createUiStringsApi(() => mockApiClient);

test('getAvailableLanguages', async () => {
// Simulate initial machine state:
mockApiClient.getAvailableLanguages.mockResolvedValueOnce([]);

const { result } = renderHook(() => api.getAvailableLanguages.useQuery(), {
wrapper: QueryWrapper,
});

await waitFor(() => expect(result.current.isSuccess).toEqual(true));
expect(result.current.data).toEqual([]);

// Simulate configuring an election:
await act(async () => {
mockApiClient.getAvailableLanguages.mockResolvedValueOnce([
LanguageCode.CHINESE,
LanguageCode.SPANISH,
]);
await api.onMachineConfigurationChange(queryClient);
});

await waitFor(() => expect(result.current.isLoading).toEqual(false));
expect(result.current.data).toEqual([
LanguageCode.CHINESE,
LanguageCode.SPANISH,
]);

// Simulate unconfiguring an election:
await act(async () => {
mockApiClient.getAvailableLanguages.mockResolvedValueOnce([]);
await api.onMachineConfigurationChange(queryClient);
});

await waitFor(() => expect(result.current.isLoading).toEqual(false));
expect(result.current.data).toEqual([]);
});
Loading

0 comments on commit 8b41fda

Please sign in to comment.