snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View File

@@ -0,0 +1,267 @@
---
title: "Actions"
sidebar_position: 4
---
In Redux, actions represent an operation either performed by the user or the server that cause a change to the state of the web app which is stored in the Redux store. It's generally represented as a plain JavaScript object with a constant `type` string with other data stored in fields such as `data`.
```typescript
{
type: 'SELECT_CHANNEL',
data: channelId,
}
```
Actions are created by functions called action creators. In regular Redux, this function will take some arguments and return an action representing how the store should be changed. Something to note with Mattermost Redux is that we typically refer to the action creators as the "actions" themselves since there's often a single action creator for a given type of action.
```typescript
function selectChannel(channelId: string) {
return {
type: 'SELECT_CHANNEL',
data: channelId,
};
}
```
This action is later received by the Redux store's reducers which will know how to read the contents of the action and modify the store accordingly.
Because we use the [Thunk middleware](https://github.com/reduxjs/redux-thunk) for Redux, we have the ability to use more powerful action creators that can read the state of the store, perform asynchronous actions like network requests, and dispatch multiple actions when needed. Instead of returning a plain object, these action creators return a function that takes the Redux store's `dispatch` and `getState` to be able to dispatch actions as needed.
```typescript
function loadAndSelectChannel(channelId: string) {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const {channels} = getState().entities.channels;
if (!channels.hasOwnProperty(channelId)) {
// Optionally call another action to asynchronously load the channel over the network
dispatch(setChannelLoading(true));
await dispatch(loadChannel(channelId));
dispatch(setChannelLoading(false));
}
// Switch to the channel
dispatch(selectChannel(channelId));
};
}
```
Actions live in the `src/actions` directory with the constants that define their types being in the `src/action_types` directory.
## Use actions
To use an action, you need to pass it into the `dispatch` method of the Redux store so that it can be passed off to the reducers.
```typescript
const store = createReduxStore();
store.dispatch(loadAndSelectChannel(channelId));
```
Typically, you won't have direct access to the store to get its `dispatch` method. Instead, you'll receive it from either [React Redux](https://react-redux.js.org/) or [Redux Thunk](https://github.com/reduxjs/redux-thunk) depending on what part of the code you're working on.
### Dispatch actions from a component
[React Redux](https://react-redux.js.org/) provides two ways of accessing dispatch, and you'll see both used throughout Mattermost.
The first is by its `connect` higher order component. Its second parameter `mapDispatchToProps` is used to wrap action creators so that they will automatically be dispatched when called.
```tsx
// src/components/widget/index.jsx
import {connect} from 'react-redux';
import {loadAndSelectChannel} from 'src/actions/channels';
import Widget from './widget';
// mapDispatchToProps is an object containing all actions passed into the component
const mapDispatchToProps = {
loadAndSelectChannel,
};
export default connect(null, mapDispatchToProps)(Widget);
// src/components/widget/widget.tsx
type Props = {
channelId: string;
// Notice that the type of the wrapped action omits the `getState` and `dispatch` parameters of the Thunk action
loadAndSelectChannel: (channelId: string) => void;
}
export default function Widget(props: Props) {
const handleClick = useCallback(() => {
// We don't need to dispatch anything at this point
props.loadAndSelectChannel(props.channelId);
}, [props.loadAndSelectChannel, props.channelId]);
return (
<button onClick={handleClick}>
{'Click me!'}
</button>
);
}
```
Alternatively, you can use the `useDispatch` hook to dispatch actions directly in the component.
```tsx
// src/components/widget/widget.tsx
import {useDispatch} from 'react-redux';
import {loadAndSelectChannel} from 'src/actions/channels';
type Props = {
channelId: string;
}
export default function Widget(props: Props) {
const dispatch = useDispatch();
const handleClick = useCallback(() => {
dispatch(loadAndSelectChannel(props.channelId));
}, [dispatch, props.channelId]);
return (
<button onClick={handleClick}>
{'Click me!'}
</button>
);
}
```
The choice of which method to use is left up to the developer at the moment. `connect` is more widely used throughout the code base, but that's primarily because hooks are relatively new compared to it. The class-based components that make up older parts of the app also aren't compatible with hooks.
When deciding which one to use though, try to match the area of the code that you're working in. Individual components should never mix the two.
## Add an action
The steps for adding a new Redux action are as follows:
1. Decide where to the action creator will be located. Depending on where the action will be located you will want to put it in one of the following locations:
- If the action is more general and affects Redux state stored in `state.entities`, it should be put somewhere in `webapp/channels/src/packages/mattermost-redux/src/actions`.
- If the action is specific to the web app, affects `state.views` and will be used in multiple places throughout the app, it should be put in `actions`.
- If the action is very specific and will likely only be used by one or more closely related components, it should be put in an `actions.ts` located in the same directory as those components.
2. If the action creator will have an effect on the Redux state that isn't covered by existing action types, you'll need to add a new "action type" constant that will be used by the action creator and will be handled by a reducer. These are located separate from the definition of the action creator itself to avoid having reducers import code from the action creators directly.
Depending on where the action is located, the action creator will be located in one of the following:
- If the action is located in `mattermost-redux`, the action type should be added to one of the files in `webapp/channels/src/packages/mattermost-redux/src/action_types`.
- If the action is specific to the web app or a single component, the action type should be added to the `ActionTypes` object in `webapp/channels/src/utils/constants.tsx`.
```typescript
export default keyMirror({
SOMETHING_HAPPENED: null
});
```
3. Write the action creator itself. Depending on what data is needed by the action and if it needs to perform any async operations will change whether or not a Thunk action should be used. We should generally try to use plain Redux actions wherever possible since they're a bit more complex, both to read and to process.
```typescript
function somethingHappened(channelId: string) {
return {
type: SOMETHING_HAPPENED,
channelId,
data: 1234,
};
}
function somethingAsyncHappened(channelId: string) {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const currentUserId = getCurrentUserId(getState());
let data;
try {
data = await Client4.doSomething(currentUserId, channelId);
} catch (error) {
dispatch({
type: SOMETHING_FAILED,
channelId,
error,
});
return {error};
}
// Note that if you need to access state again after waiting for something asynchronous, you should call
// getState a second time to ensure you have an up to date version of the state
dispatch({
type: SOMETHING_HAPPENED,
channelId,
data,
});
};
}
```
4. If you added a new action type, make sure to add or update existing reducers to handle the new action. More information about reducers is available [here](/developers/contribute/more-info/webapp/redux/reducers).
5. Add unit tests to make sure that the action has the intended effects on the store. More information on unit testing reducers is available on the page [Redux Unit and E2E Testing](/developers/contribute/more-info/webapp/redux/testing).
### Add a new API action
If your action is corresponds to an API call, there are a few extra steps required but also a helper function to simplify the error handling for the action. The additional steps are as follows:
1. Ensure that `Client4`, the JavaScript API client for Mattermost which is located in `webapp/platform/client/src/client4.ts`, has a method that corresponds to the API endpoint that you're using. That method will likely involve simply constructing the URL for the endpoint, optionally constructing a body for the request, and then using the `doFetch` method to actually make the request.
```typescript
class Client4 {
doSomething = (userId: string, channelId: string) => {
return this.doFetch<SomethingResponse>(
`${this.getUserRoute(userId)}/something`,
{method: 'post', body: JSON.stringify({channelId})},
);
}
}
```
2. Depending on your use case, you'll likely want to dispatch a Redux action containing the response to the API request when it succeeds. You may optionally also want to dispatch actions when the request is made or fails to update the Redux state as the request progresses.
```typescript
export default keyMirror({
SOMETHING_HAPPENED: null,
// The following actions are optional. They used to be added for every API request, but we found we were only
// rarely using their results, so we don't recommend adding them any more
SOMETHING_REQUEST: null,
SOMETHING_SUCCESS: null,
SOMETHING_FAILURE: null,
});
```
3. Most actions involving an API request follow a similar pattern of calling Client4 with the provided parameters, handling any errors that may occur, and dispatching an action containing the result if successful. The `bindClientFunc` helper can help with that.
```typescript
function somethingAsyncHappened(channelId: string) {
return bindClientFunc({
clientFunc: client.doSomething,
onSuccess: SOMETHING_HAPPENED,
params: [channelId],
};
}
// clientFunc is the only mandatory parameter of bindClientFunc. The rest may be added as needed.
function somethingVerboseHappened(userId: string, channelId: string) {
return bindClientFunc({
clientFunc: client.doSomething,
// The onRequest action will be dispatched before the request is made
onRequest: SOMETHING_REQUEST,
// The onSuccess action will be dispatched if the request succeeds. It will include a data parameter
// containing the response to the request. Additionally, onSuccess can be an array of actions if multiple
// should be dispatched when the request succeeds.
onSuccess: [SOMETHING_SUCCESS, SOMETHING_HAPPENED],
// The onFailure action will be dispatched if the request fails due to a network issue or an invalid request.
// It will include an error parameter containing an Error object.
onFailure: SOMETHING_FAILED,
// An array of parameters will be passed into clientFunc in the order they're received
params: [userId, channelId],
};
}
```

View File

@@ -0,0 +1,12 @@
---
title: "Redux"
sidebar_position: 8
---
The Mattermost web app uses [Redux](https://redux.js.org/) as its state management library. Its key features are a centralized data store for the entire app and a pattern for predictably modifying and displaying that application state. Notably, we're not using Redux Toolkit since a large portion of our Redux code predates its existence.
In addition to Redux itself, we also use:
- [React Redux](https://react-redux.js.org/) to connect React components to the Redux store using higher-order components like `connect` or hooks like `useSelector`.
- [Redux Thunk](https://github.com/reduxjs/redux-thunk) to write async actions and logic that interacts more closely with the Redux store.
Currently, the different packages in the web app use Redux in varying amounts. The bulk of our Redux code is in `channels` where it's split between logic that's more view-oriented, located at the root of its `src` directory, and logic that's more server-oriented, located in `channels/src/packages/mattermost-redux`.

View File

@@ -0,0 +1,93 @@
---
title: "Use Redux with React"
sidebar_position: 7
---
Using Redux with React is fairly straightforward thanks to the [React Redux](https://github.com/reactjs/react-redux) library. It provides the `connect` function to create higher order components that have access to the Redux store to set their props.
A typical Redux-connected component will be in its own folder with two files: `index.jsx` containing the code to connect to the Redux store and the file where the component is actually implemented. This helps to keep the Redux logic separate from the rendering for the component which keeps it more easily readable and makes it easier to test since it can be done without the whole Redux store.
```jsx
// components/my_component/index.jsx
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {messageUser} from 'actions/entities/users';
import {getCurrentUser, getUser} from 'selectors/entities/users';
import MyComponent from './my_component';
// mapStateToProps receives the Redux store state and any props passed into the connected
// component, and they are used to return any additional data from the Redux store that is
// needed to render the component. ownProps will also be passed directly to the component.
function mapStateToProps(state, ownProps) {
return {
currentUser: getCurrentUser(state),
otherUser: getUser(state, ownProps.userId),
};
}
// mapDispatchToProps receives the Redux store's dispatch method so that bindActionCreators
// can be used to automatically dispatch those actions as necessary.
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
messageUser,
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);
// components/my_component/my_component.jsx
import React from 'react';
function MyComponent(props) {
const handleClick = () => {
props.actions.messageUser(props.otherUser, props.currentUser, `Hello, ${props.otherUser.first_name}!`);
};
return (
<label>
{`${props.otherUser.first_name} ${props.otherUser.last_name}: `}
<button onClick={this.handleClick}>{'Say Hi'}</button>
</label>
);
}
```
Both `mapStateToProps` and `mapDispatchToProps` are optional and can be omitted as necessary.
If you're using a selector that is produced through a factory, such as `makeGetUser`, you can instead generate an individual `mapStateToProps` function for each instance of the component.
```jsx
// component/my_component/index.jsx
...
import {getCurrentUser, makeGetUser} from 'selectors/entities/users';
// makeMapStateToProps is called once for each instance of the component on the page. Because of this
// a separate getUser selector is created for each instance, allowing them to be memoized separately.
function makeMapStateToProps() {
const getUser = makeGetUser();
return (state, ownProps) => {
return {
currentUser: getCurrentUser(state),
otherUser: getUser(state, ownProps.userId)
};
};
}
...
export default connect(makeMapStateToProps, mapDispatchToProps)(MyComponent);
```
## Performance considerations
Something very important to note when using React with Redux is that every single `mapStateToProps` function within your application will be called whenever anything in the store changes. If any work being done in `mapStateToProps` performs any complicated calculations or returns rich objects, it should be moved into a [selector](/developers/contribute/more-info/webapp/redux/selectors) so that it can be memoized whenever possible.

View File

@@ -0,0 +1,107 @@
---
title: "Reducers"
sidebar_position: 5
---
Reducers in Redux are pure functions that describe how the data in the store changes after any given action. A reducer receives the previous state of the store and an action as a JavaScript object (see [here](/developers/contribute/more-info/webapp/redux/actions) for more information on actions) and should output the resulting state without receiving any outside data. Because reducers are pure, they will always produce the same resulting state for a given state and action.
```javascript
const myValueDefault = '';
function myValue(state = myValueDefault, action) {
switch(action.type) {
case SET_MY_VALUE:
// This data changes myValue, so just return the new value
return action.data;
default:
// This action doesn't affect us, so return the previous value
return state;
}
}
```
Most reducers used by Mattermost Redux are simple like the one above in that they only affect a single part of the store. These can be easily composed using Redux's [`combineReducers`](https://redux.js.org/api-reference/combinereducers) function to make a more complex data store.
```javascript
function myStringValue(state = '', action) {
...
}
function myNumberValue(state = 0, action) {
...
}
function myOtherValue(state = {}, action) {
...
}
// This combines the reducers so that the resulting store will look like
// {
// myStringValue: 'abc',
// myNumberValue: '1234',
// myOtherValue: {color: 'red', weather: 'rain'}
// }
export const combineReducers({
myStringValue,
myNumberValue,
myOtherValue
});
```
`combineReducers` can be nested further to make up the complex data store as used by Mattermost Redux.
## Avoiding mutating the store
One of the core principals of Redux is that the state of the store should never be modified. If the state changes, a completely new state tree should be returned. That's not to say the entire thing is destroyed and recreated from scratch any time anything changes, but only the parts that are modified are recreated.
For example, if the store holds a state like:
- entities
- channels
- channels
- users
- currentUserId
- profiles
- requests
- getUser
- getChannel
and we make a request to get the profile for a user that we don't have, the following fields would be changed (shown in bold):
- **entities**
- channels
- channels
- **users**
- currentUserId
- **profiles**
- **requests**
- **getUser**
- getChannel
To do this, it is important to always return new objects from a reducer if any of its contents change. This is trivial for reducers for state that is a primitive string or number, but it can be more complicated for other types of data. You should also make sure to return the same object if nothing needs to change. As long as you do that, `combineReducers` will make sure to update the rest of the state tree accordingly.
```javascript
function myOtherValue(state = {color: 'red', weather: 'rain'}, action) {
switch(action.type) {
case SET_OTHER_VALUE_COLOR:
// This destructuring syntax is used to create a new object that is a shallow copy of state
// with the color field updated to the new value
return {
...state,
color: action.data
};
case SET_OTHER_VALUE_WEATHER:
return {
...state,
weather: action.data
};
default:
// There's no changes, so return the previous value
return state;
}
...
}
```
If you accidentally mutate the state, you'll receive an error when Mattermost Redux is running in development mode.

View File

@@ -0,0 +1,82 @@
---
title: "Selectors"
sidebar_position: 6
---
Selectors are functions used to compute data from the data in the Redux stores. This is done using [Reselect](https://github.com/reactjs/reselect), a library designed to do this efficiently by memoizing any results so that they are only recalculated if relevant parts of the store change. The code for this is in the `src/selectors` folder of the Mattermost Redux repository.
For more information about reselect and how we use it at Mattermost, [check out this developer talk given by core developer Harrison Healey](https://www.youtube.com/watch?v=6N2X7gEwmaQ).
## Use a selector
Selectors are simple functions that take the state from the redux store and return some data computed from them. Most selectors take simply the state of the redux store and return some data from the store.
```javascript
const currentUser = getCurrentUser(state);
const currentTeam = getCurrentTeam(state);
```
Selectors can also receive additional arguments however extra care must be taken to ensure that the extra argument doesn't prevent proper memoization. Instead of providing the selector directly, a factory function will usually be provided so that each location or component that users the selector can be memoized separately.
```javascript
const getPostsInThread = makeGetPostsInThread();
const selectedPost = getSelectedPost(state);
const postsInSelectedThread = getPostsInThread(state, selectedPost.root_id);
```
For an example of how a selector like `makeGetPostInThread` could be used with React, see [here](/developers/contribute/more-info/webapp/redux/react-redux).
## Add a selector
The most basic selector is just a function that takes in the state and returns a part of it without any memoization or complicated logic. All these provide is that they make it easier to access part of the store without having to remember exactly where the data is.
```javascript
export function getCurrentUserId(state) {
return state.entities.users.currentUserId;
}
```
To create a selector that actually computes some data, you need to use Reselect's [`createSelector`](https://github.com/reactjs/reselect#createselectorinputselectors--inputselectors-resultfunc) to combine simple selectors like the one above and provide some proper memoziation.
```javascript
export const getCurrentUser = createSelector(
getCurrentUserId,
(state) => state.entities.users.profiles,
(currentUserId, profiles) => {
if (!profiles.hasOwnProperty(currentUserId)) {
// Current user not found
return {};
}
return profiles[currentUserId];
}
);
```
The way that `createSelector` works is that it takes any number of "input selectors" followed by a single "result function". When you call the selector, it calls all of the input selectors to get the values that will be passed into the result function. If any of those have changed, it passes them to the result function to calculate and return the final result. If none of those values change, it skips the result function, and just returns the previous value.
By keeping the input selectors simple and memoizing based on their results, we can skip recalculating the final value which saves time and keeps it so that `getCurrentUser(state) === getCurrentUser(state)` even when `getCurrentUser` returns an object that can't normally be compared with `===`. This is incredibly important when using Redux with React as your selectors will likely be called many times per second so minimizing time taken and returning new objects sparingly can have significant performance gains.
While Reselect doesn't encourage the use of selectors with parameters, these can be passed in when calling the selector. Any arguments passed in will be provided to the input selectors, but not the result function. If necessary, you can work around this by including an extra input selector to pass the argument along.
```javascript
export function makeGetUser() {
return createSelector(
getProfiles,
(state, userId) => userId,
(profiles, userId) => {
if (!profiles.hasOwnProperty(userId)) {
// User not found
return {};
}
return profiles[userId];
}
);
}
```
Note that when we make a selector that takes arguments, we typically wrap it in a factory function so that we can create multiple instances of the selector that are each memoized separately. This is because having a single instance of the above `getUser` selector and calling it with different user IDs would prevent any memoization since it would be constantly called with different IDs leading it to recalculate on each call.
This may sound unnecessary if you're writing a one-off selector, but if you think of something like a `getPost` selector in the Mattermost app, we will frequently be rendering 100+ post components each with their own copy of the `getPost` selector. With only a single copy of that selector, it would be constantly recalculating, but with 100+ copies, each only recalculates and rerenders when their specific post changes.

View File

@@ -0,0 +1,102 @@
---
title: "Redux Testing"
sidebar_position: 7
---
### Unit Testing
#### Unit Testing for Actions
Tests for both actions and action creators are written using [Jest](https://jestjs.io/) and will often focus on seeing how dispatching an action affects the stored state in Redux. It'll often look similar to testing a reducer except you'll be looking at the whole store state instead of a single part of it.
There are a few different ways of testing Redux actions used throughout Mattermost, but the most common way involves:
1. Setting up an initial store state for the test case.
2. Optionally mocking any external operations that may be required for the action. This includes API requests which are mocked using [Nock](https://github.com/nock/nock).
3. Dispatching the result of the action creator.
4. Looking at the resulting store state to ensure the required changes are made.
```typescript
import nock from 'nock';
import mockStore from 'tests/test_store';
import {somethingAsyncHappened, somethingHappened} from './actions';
describe('somethingHappened', () => {
const channelId = 'channelId';
test('should update state.somethingCount', () => {
const store = mockStore({
somethingCount: 0,
});
// Remember to actually call your action creator since that's very easy to forget to do
store.dispatch(somethingHappened(channelId));
expect(store.getState().somethingCount).toBe(1234);
});
});
describe('somethingAsyncHappened', () => {
// Initial state may be shared between multiple test cases and may include state that's required for both
// testing and for thunk actions
const currentUserId = 'currentUserId';
const initialState = {
entities: {
users: {
currentUserId: 'user1',
},
},
somethingCount: 0,
};
test('should update state.somethingCount on success', async () => {
const store = mockStore(initialState);
const expectedResult = {status: 'SomethingHappened'};
nock(Client4.getBaseRoute()).
post(`/channels/${channelId}/something`).
reply(200, {});
// Remember that tests for async requests need to themselves be async and we need to wait for the dispatch
await store.dispatch(somethingAsyncHappened(channelId));
expect(store.getState().somethingCount).toBe(1234);
});
test('should update state.somethingCount on failure', async () => {
const store = mockStore(initialState);
const expectedResult = {status: 'SomethingHappened'};
nock(Client4.getBaseRoute()).
post(`/channels/${channelId}/something`).
reply(400, {});
// You can also inspect the result of the action if desired
const result = await store.dispatch(somethingAsyncHappened(channelId));
expect(result.error).toBeDefined();
expect(result.data).not.toBeDefined();
expect(store.getState().somethingCount).toBe(0);
});
});
```
5. Add unit tests to make sure that the action has the intended effects on the store. Test location is adjacent to the file being tested. Example, for `src/actions/admin.js`, test is located at `src/actions/admin.test.js`. Add test file if necessary. More information on unit testing reducers is available below.
Some unit tests found throughout the web app may also test the actions dispatched by a thunk action rather than testing the effects on the changes to the store state. This method isn't considered as effective.
#### Unit Testing for Selectors
Unit tests for selectors are located in the same directory, adjacent to the file being tested. For example, the test for `src/selectors/admin.js` is located at `src/selectors/admin.test.js`. These tests are written using [Jest Testing Framework](https://jestjs.io/). In that folder, there are many examples of how those tests should look. Most follow the same general pattern of:
1. Construct the initial test state. Note that this doesn't need to be shared between tests as it is in many other cases.
2. Pass the state into the selector and check the results. The tests for some more complicated selectors do this multiple times while changing different parts of the store to ensure that the memoization is working correctly since it can be very important in certain areas of the app.
### End-to-End (E2E) Testing
#### E2E Tests for Actions
Sometimes, it's not easy to test a redux action given it contains complicated async logic or requires a large amount of Redux state to be initialized to test it out. Other times, an action may feel too simple to test, especially if it's just dispatching an action that dictates specifically how the Redux state should change.
In cases where the action will have an effect that's visible to the end user, it's possible to rely more on [end-to-end testing](/developers/contribute/more-info/webapp/e2e-testing). While this might not test every code path of the action such as poor network conditions, end-to-end tests are often more valuble since they involve testing that the code as a whole does what is expected rather than testing just that a single piece of code works under artificial conditions which may not be realistic.