Skip to content

Commit

Permalink
Switch to using Redux integration tests and remove mock-redux-store (#…
Browse files Browse the repository at this point in the history
…127)

Also adds a containerized-test target to re-run JS tests w/o rebuilding (mounts in changes)
  • Loading branch information
wcjordan authored Mar 8, 2024
1 parent 25e4a13 commit 0ef2dc8
Show file tree
Hide file tree
Showing 19 changed files with 499 additions and 549 deletions.
13 changes: 1 addition & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,7 @@ build:
.PHONY: test
test: build
DOMAIN=localhost docker run --env-file .env --env DOMAIN --rm -t $(SERVER_IMAGE):local-latest make test
docker run --rm -t -w / \
-v $(PWD)/ui/Makefile:/Makefile \
-v $(PWD)/ui/js/src:/js/src \
-v $(PWD)/ui/js/assets:/js/assets \
-v $(PWD)/ui/js/.eslintrc:/js/.eslintrc \
-v $(PWD)/ui/js/babel.config.js:/js/babel.config.js \
-v $(PWD)/ui/js/jest.config.js:/js/jest.config.js \
-v $(PWD)/ui/js/tsconfig.json:/js/tsconfig.json \
-v $(PWD)/ui/js/.storybook:/js/.storybook \
-v $(PWD)/ui/js/__mocks__:/js/__mocks__ \
$(UI_IMAGE_BASE):local-latest \
make test
$(MAKE) -C ui containerized-test

# Run integration tests
# Requires dev env to be running (make start)
Expand Down
15 changes: 15 additions & 0 deletions ui/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ test:
# Unit & snapshot tests for UI
cd js; JEST_JUNIT_CLASSNAME="ui_unit_tests.{classname} {title}" yarn jest

.PHONY: containerized-test
containerized-test:
docker run --rm -t -w / \
-v $(CURDIR)/Makefile:/Makefile \
-v $(CURDIR)/js/src:/js/src \
-v $(CURDIR)/js/assets:/js/assets \
-v $(CURDIR)/js/.eslintrc:/js/.eslintrc \
-v $(CURDIR)/js/babel.config.js:/js/babel.config.js \
-v $(CURDIR)/js/jest.config.js:/js/jest.config.js \
-v $(CURDIR)/js/tsconfig.json:/js/tsconfig.json \
-v $(CURDIR)/js/.storybook:/js/.storybook \
-v $(CURDIR)/js/__mocks__:/js/__mocks__ \
$(UI_IMAGE_BASE):local-latest \
make test

# ESLint & Prettier JS formatting
.PHONY: format
format:
Expand Down
4 changes: 2 additions & 2 deletions ui/js/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Provider as ReduxProvider } from 'react-redux';
import { init as sentryInit } from 'sentry-expo';

import App from './src/App';
import getStore from './src/redux/store';
import { setupStore } from './src/redux/store';
import { getEnvFlags } from './src/helpers';

const envFlags = getEnvFlags();
Expand Down Expand Up @@ -38,7 +38,7 @@ const theme = {
const TopApp: React.FC = function () {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<ReduxProvider store={getStore()}>
<ReduxProvider store={setupStore()}>
<PaperProvider theme={theme}>
<App />
</PaperProvider>
Expand Down
1 change: 0 additions & 1 deletion ui/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
"react-native-web": "~0.19.9",
"react-redux": "^8.1.3",
"redux": "^4.2.1",
"redux-mock-store": "^1.5.4",
"redux-thunk": "^2.4.2",
"sentry-expo": "~7.2.0"
},
Expand Down
15 changes: 8 additions & 7 deletions ui/js/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { connect, ConnectedProps, useSelector } from 'react-redux';
import { connect, ConnectedProps } from 'react-redux';
import React from 'react';
import { StatusBar, StyleSheet, View, ViewStyle } from 'react-native';

import ErrorBar from './components/ErrorBar';
import Login from './components/Login';
import TodoList from './components/TodoList';
import { useAppSelector } from './hooks';
import {
Label,
MoveTodoOperation,
ReduxState,
Todo,
TodoPatch,
WorkspaceState,
Expand All @@ -28,6 +28,7 @@ import {
updateTodo,
updateTodoLabels,
} from './redux/reducers';
import { RootState } from './redux/store';
import {
selectActiveWorkContext,
selectFilteredTodos,
Expand Down Expand Up @@ -58,10 +59,10 @@ const styles = StyleSheet.create<Style>({
const App: React.FC<ConnectedProps<typeof connector>> = function (
props: ConnectedProps<typeof connector>,
) {
const selectedPickerLabels = useSelector(selectSelectedPickerLabels);
const filteredTodos = useSelector(selectFilteredTodos);
const activeWorkContext = useSelector(selectActiveWorkContext);
const isLoading = useSelector(selectIsLoading);
const selectedPickerLabels = useAppSelector(selectSelectedPickerLabels);
const filteredTodos = useAppSelector(selectFilteredTodos);
const activeWorkContext = useAppSelector(selectActiveWorkContext);
const isLoading = useAppSelector(selectIsLoading);
return (
<AppLayout
{...props}
Expand Down Expand Up @@ -150,7 +151,7 @@ type LayoutProps = {
workspace: WorkspaceState;
};

const mapStateToProps = (state: ReduxState) => {
const mapStateToProps = (state: RootState) => {
return {
labels: state.labelsApi.entries,
workspace: state.workspace,
Expand Down
99 changes: 0 additions & 99 deletions ui/js/src/hooks.test.ts

This file was deleted.

98 changes: 98 additions & 0 deletions ui/js/src/hooks.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { cleanup, renderHook } from '@testing-library/react-hooks';
import { waitFor } from '@testing-library/react';
import fetchMock from 'fetch-mock-jest';
import { Provider } from 'react-redux'
import React, { PropsWithChildren } from 'react';

import { useDataLoader } from './hooks';
import { setupStore } from './redux/store';

// Pretend not a test so useDataLoader doesn't bail on requests
jest.mock('expo-constants', () => ({
expoConfig: {
extra: {
ENVIRONMENT: 'not_test',
},
},
}));

describe('useDataLoader', function () {
afterEach(function () {
fetchMock.restore();
});

it('should setup loading labels and todos', async function () {
const labelEntries = [{
id: 1,
name: "home"
}, {
id: 2,
name: "work"
}];
fetchMock.getOnce('http://chalk-dev.flipperkid.com/api/todos/labels/', {
body: labelEntries,
});

const firstTodo = {
id: 256,
description: "First Todo",
};
const secondTodo = Object.assign({}, firstTodo, {
id: 257,
description: "Second Todo"
});
const todosRoute = 'http://chalk-dev.flipperkid.com/api/todos/todos/';
fetchMock.getOnce(todosRoute, {
body: [firstTodo],
});

jest.useFakeTimers();

const store = setupStore();
const wrapper = ({ children }: PropsWithChildren<object>): JSX.Element => <Provider store={store}>{children}</Provider>;
renderHook(useDataLoader, { wrapper });

// Verify loading labels started
expect(store.getState().labelsApi.loading).toEqual(true);

// Verify loading labels completed
await waitFor(() => {
expect(store.getState().labelsApi.loading).toEqual(false);
expect(store.getState().labelsApi.entries).toEqual(labelEntries);
});

// Move forward long enough to load Todos
jest.advanceTimersByTime(10000);

// Verify loading todos completed
expect(store.getState().todosApi.loading).toEqual(true);
await waitFor(() => {
expect(store.getState().todosApi.loading).toEqual(false);
expect(store.getState().todosApi.entries).toEqual([firstTodo]);
});

// Move forward long enough to load Todos again
fetchMock.get(todosRoute, {
body: [firstTodo, secondTodo],
}, { overwriteRoutes: true });
jest.advanceTimersByTime(10000);

// Verify todos updated with a 2nd after loading again
expect(store.getState().todosApi.loading).toEqual(true);
await waitFor(() => {
expect(store.getState().todosApi.loading).toEqual(false);
expect(store.getState().todosApi.entries).toEqual([firstTodo, secondTodo]);
});

// Ensure no more Todos are loaded after cleanup
cleanup();
jest.advanceTimersByTime(30000);
await fetchMock.flush();
expect(fetchMock.calls().length).toEqual(3);

// Verify we make the server requests
expect(fetchMock).toBeDone();
}, 20000);
});

export {};
13 changes: 9 additions & 4 deletions ui/js/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect } from 'react';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';

import getStore from './redux/store';
import type { RootState, AppDispatch } from './redux/store';
import { listLabels, listTodos } from './redux/reducers';
import { getEnvFlags } from './helpers';

Expand All @@ -9,12 +10,12 @@ export function useDataLoader() {
return;
}

const dispatch = useAppDispatch();
useEffect(() => {
const store = getStore();
store.dispatch(listLabels());
dispatch(listLabels());

const intervalId = window.setInterval(
() => store.dispatch(listTodos()),
() => dispatch(listTodos()),
10000,
);

Expand All @@ -23,3 +24,7 @@ export function useDataLoader() {
};
}, []);
}

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
4 changes: 2 additions & 2 deletions ui/js/src/redux/fetchApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import Cookies from 'js-cookie';
import { Platform } from 'react-native';

import { getEnvFlags } from '../helpers';
import { ReduxState } from './types';
import { RootState } from './store';

const webCsrfToken = Cookies.get('csrftoken') as string;
export function getCsrfToken(getState: () => ReduxState): string {
export function getCsrfToken(getState: () => RootState): string {
if (Platform.OS === 'web') {
return webCsrfToken;
}
Expand Down
Loading

0 comments on commit 0ef2dc8

Please sign in to comment.