-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Experiment: TanStack Query #39
base: develop
Are you sure you want to change the base?
Changes from all commits
eed7c41
4601391
e498488
67c7590
edb413c
c92f6e0
325c72f
5ae5054
cc05b21
396aa55
af15f6b
d63ff9f
8be5879
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,44 +1,21 @@ | ||
import { REHYDRATE } from 'redux-persist' | ||
import { expectSaga } from 'redux-saga-test-plan' | ||
import * as matchers from 'redux-saga-test-plan/matchers' | ||
import { throwError } from 'redux-saga-test-plan/providers' | ||
import { resetStore } from '@redux/rootActions' | ||
import { Failure, Loading, RemoteDataStates, Success } from '@utils/api' | ||
import { logIn } from './auth' | ||
import { | ||
authSlice, | ||
logInAsync, | ||
logInAsyncFailure, | ||
logInAsyncSuccess, | ||
watchAuthTokens, | ||
watchLogInSaga, | ||
} from './authSlice' | ||
import { authSlice, logInAsyncSuccess, watchAuthTokens } from './authSlice' | ||
import { setAuthConfig } from './common' | ||
|
||
const fakeAuthTokens = { | ||
accessToken: 'FAKE_ACCESS_TOKEN', | ||
refreshToken: 'FAKE_REFRESH_TOKEN', | ||
} | ||
const fakeCredentials = { username: 'FAKE_USERNAME', password: 'FAKE_PASSWORD' } | ||
const logInErrorMessage = 'Login failed' | ||
|
||
describe('#watchAuthTokens', () => { | ||
it('should set API auth tokens on successful login', () => { | ||
return expectSaga(watchAuthTokens) | ||
.call(setAuthConfig, fakeAuthTokens) | ||
.dispatch(logInAsyncSuccess(fakeAuthTokens)) | ||
.silentRun() | ||
}) | ||
|
||
it('should restore API auth tokens on REHYDRATE auth action', () => { | ||
const rehydrateAction = { | ||
type: REHYDRATE, | ||
key: 'auth', | ||
payload: { | ||
tokens: { | ||
data: fakeAuthTokens, | ||
type: RemoteDataStates.SUCCESS, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need to store and test request state, because it's implemented and tested in |
||
}, | ||
tokens: fakeAuthTokens, | ||
_persist: { | ||
rehydrated: true, | ||
version: -1, | ||
|
@@ -63,45 +40,13 @@ describe('#watchAuthTokens', () => { | |
}) | ||
}) | ||
|
||
describe('#watchLogInSaga', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tests of logging in logic are moved to tests of |
||
it('should log user in and put success action with tokens', () => { | ||
return expectSaga(watchLogInSaga) | ||
.provide([[matchers.call.fn(logIn), fakeAuthTokens]]) | ||
.put(logInAsyncSuccess(fakeAuthTokens)) | ||
.dispatch(logInAsync(fakeCredentials)) | ||
.silentRun() | ||
}) | ||
|
||
it('should put log in error action wit error message on auth error', () => { | ||
return expectSaga(watchLogInSaga) | ||
.provide([[matchers.call.fn(logIn), throwError(new Error(logInErrorMessage))]]) | ||
.put(logInAsyncFailure(logInErrorMessage)) | ||
.dispatch(logInAsync(fakeCredentials)) | ||
.silentRun() | ||
}) | ||
}) | ||
|
||
describe('#authSlice', () => { | ||
const initialState = authSlice.getInitialState() | ||
|
||
describe('#loginAsync', () => { | ||
it('should change tokens state to Loading', () => { | ||
const state = authSlice.reducer(initialState, logInAsync(fakeCredentials)) | ||
expect(state.tokens).toEqual(Loading) | ||
}) | ||
}) | ||
|
||
describe('#logInAsyncSuccess', () => { | ||
it('should store auth tokens', () => { | ||
const state = authSlice.reducer(initialState, logInAsyncSuccess(fakeAuthTokens)) | ||
expect(state.tokens).toEqual(Success(fakeAuthTokens)) | ||
}) | ||
}) | ||
|
||
describe('#logInAsyncFailure', () => { | ||
it('should store auth error message', () => { | ||
const state = authSlice.reducer(initialState, logInAsyncFailure(logInErrorMessage)) | ||
expect(state.tokens).toEqual(Failure(logInErrorMessage)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed those tests because there is no need to test switching states: |
||
expect(state.tokens).toEqual(fakeAuthTokens) | ||
}) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
import { all } from 'typed-redux-saga' | ||
import { watchAuthTokens, watchLogInSaga } from '@api/authSlice' | ||
import { watchGetLatestComicSaga } from '@screens/demoSlice' | ||
import { watchAuthTokens } from '@api/authSlice' | ||
|
||
export default function* rootSaga() { | ||
yield* all([watchAuthTokens(), watchLogInSaga(), watchGetLatestComicSaga()]) | ||
yield* all([watchAuthTokens()]) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { renderHook, waitFor } from '@testing-library/react-native' | ||
import { logIn } from '@api/auth' | ||
import { logInAsyncSuccess } from '@api/authSlice' | ||
import { setAuthConfig } from '@api/common' | ||
import * as persistence from '@redux/persistence' | ||
import { resetStore } from '@redux/rootActions' | ||
import { persistor } from '@redux/store' | ||
import { createTestEnvWrapper } from '@utils/testing' | ||
import { useLogInMutation, useLogOutMutation } from './auth' | ||
|
||
const mockCredentials = { | ||
username: 'testUsername', | ||
password: 'testPassword', | ||
} | ||
|
||
const mockTokens = { | ||
accessToken: 'testAccessToken', | ||
refreshToken: 'testRefreshToken', | ||
} | ||
|
||
jest.mock('@api/auth', () => ({ | ||
logIn: jest.fn(), | ||
})) | ||
const mockLogIn = logIn as jest.MockedFunction<typeof logIn> | ||
mockLogIn.mockResolvedValue(mockTokens) | ||
|
||
jest.mock('@api/common', () => ({ | ||
setAuthConfig: jest.fn(), | ||
})) | ||
const mockSetAuthConfig = setAuthConfig as jest.MockedFunction<typeof setAuthConfig> | ||
|
||
const mockDispatch = jest.fn() | ||
jest.mock('@hooks/useAppDispatch', () => ({ | ||
__esModule: true, | ||
default: jest.fn(() => { | ||
return mockDispatch | ||
}), | ||
})) | ||
|
||
jest.mock('@redux/store', () => ({ | ||
persistor: { | ||
pause: jest.fn(), | ||
persist: jest.fn(), | ||
}, | ||
})) | ||
|
||
jest.useFakeTimers() | ||
|
||
describe('auth', () => { | ||
let wrapper: ReturnType<typeof createTestEnvWrapper> | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks() | ||
wrapper = createTestEnvWrapper({}) | ||
}) | ||
|
||
describe('useLogInMutation', () => { | ||
it('calls onSuccess', async () => { | ||
const { result } = renderHook(() => useLogInMutation(), { wrapper }) | ||
|
||
result.current.mutate(mockCredentials) | ||
|
||
await waitFor(() => expect(result.current.isSuccess).toBe(true)) | ||
|
||
expect(result.current).toMatchObject({ | ||
isSuccess: true, | ||
data: mockTokens, | ||
}) | ||
|
||
expect(mockSetAuthConfig).toHaveBeenCalledTimes(1) | ||
expect(mockSetAuthConfig).toHaveBeenLastCalledWith(mockTokens) | ||
expect(mockDispatch).toHaveBeenCalledTimes(1) | ||
expect(mockDispatch).toHaveBeenLastCalledWith(logInAsyncSuccess(mockTokens)) | ||
}) | ||
}) | ||
|
||
describe('useLogOutMutation', () => { | ||
it('calls onSuccess', async () => { | ||
const mockClearPersistence = jest.spyOn(persistence, 'clearPersistence') | ||
|
||
const { result } = renderHook(() => useLogOutMutation(), { wrapper }) | ||
|
||
result.current.mutate() | ||
|
||
await waitFor(() => expect(result.current.isSuccess).toBe(true)) | ||
|
||
expect(result.current).toMatchObject({ | ||
isSuccess: true, | ||
data: undefined, | ||
}) | ||
|
||
expect(persistor.pause).toHaveBeenCalledTimes(1) | ||
expect(mockClearPersistence).toHaveBeenCalledTimes(1) | ||
expect(mockSetAuthConfig).toHaveBeenCalledTimes(1) | ||
expect(mockSetAuthConfig).toHaveBeenLastCalledWith({}) | ||
expect(mockDispatch).toHaveBeenCalledTimes(1) | ||
expect(mockDispatch).toHaveBeenLastCalledWith(resetStore()) | ||
expect(persistor.persist).toHaveBeenCalledTimes(1) | ||
}) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This indicates remote state (server state) in terms of reflecting inside the app what's stored on some remote server (backend). It's keeping networking state - loading, errors and data from requests and responses.
It's different than
redux/
, which is client local state, that doesn't need to by synchronised with server. It's updated by local user's action or some effects from remote state updates.This structure allows to keep them separate.