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
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:
163
docs/develop/contribute/more-info/webapp/build-component.md
Normal file
163
docs/develop/contribute/more-info/webapp/build-component.md
Normal file
@@ -0,0 +1,163 @@
|
||||
---
|
||||
title: "Build a component"
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
This page describes how to build a new React component in the Mattermost web app. A new component must meet the following requirements:
|
||||
|
||||
1. Is pure, meaning that all information required to render is passed in by props.
|
||||
2. Has no direct store interaction. Use `connect` to wrap the component if needed.
|
||||
3. Has component tests.
|
||||
4. Is generic and re-usable when possible.
|
||||
5. Has documented props.
|
||||
|
||||
If none of those make any sense to you or you're new to React and Redux, then check out these links:
|
||||
|
||||
- https://react.dev/learn/
|
||||
- http://redux.js.org/
|
||||
|
||||
These requirements are discussed in more detail in the following sections.
|
||||
|
||||
## Design the component
|
||||
|
||||
The most important part of designing your component is deciding on what the props will be. Props are very much the API for your component. Think of them as a contract between your component and the users.
|
||||
|
||||
Props are read-only variables that get passed down to your component either directly from a parent component or from an `index.ts` container that connects the component to the Redux store.
|
||||
|
||||
How do you decide what props your component should have? Think about what your component is trying to display to the user. Any data you need to accomplish that should be part of the props.
|
||||
|
||||
As an example, let's imagine we're building an `ItemList` component with the purpose of displaying a list of items. The props for such a component might look like:
|
||||
|
||||
```typescript
|
||||
type Props = {
|
||||
/**
|
||||
* The title of the list
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* An array of items to display
|
||||
*/
|
||||
items: ListItem[];
|
||||
}
|
||||
```
|
||||
|
||||
The `title` prop is a string that's displayed as the title of the list, and `items` is an array of objects that make up the contents of the list. Note that `items` is required while `title` is optional.
|
||||
|
||||
Make sure you add brief but clear comments to each prop type as shown in the example.
|
||||
|
||||
Our ItemList component would live in a file named `item_list.tsx`.
|
||||
|
||||
## Use a Redux container component
|
||||
|
||||
The next question to ask yourself is whether you're going to need a container component. This is the `index.ts` file mentioned above. If your component needs either of the following, then you'll need a container:
|
||||
|
||||
1. Needs some data injected into its props that the parent component doesn't have access to
|
||||
2. Needs to be able to perform some sort of action that affects the state of the store
|
||||
|
||||
Continuing the `ItemList` example above, maybe our parent component doesn't care about our list of items and doesn't have access to them. Let's also imagine that we want to let the user remove items from the list by clicking on them. This means our component now needs a container for both criteria above and our props will change slightly:
|
||||
|
||||
```typescript
|
||||
type Props = {
|
||||
/**
|
||||
* The title of the list
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* An array of item components to display
|
||||
*/
|
||||
items: ListItem[];
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* An action to remove an item from the list
|
||||
*/
|
||||
removeItem: (item: ListItem) => void;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Note that the type definition for actions passed from Redux won't include the any reference to Redux's `dispatch` or Redux Thunk's `getState`. This is intentional as `connect` hides those details from the component.
|
||||
|
||||
The container will then handle getting data from the Redux state in `mapStateToProps` and passing Redux actions to the component using `mapDispatchToProps`. Note that either of these are optional if only one is needed.
|
||||
|
||||
```typescript
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {removeItem} from 'mattermost-redux/actions/items';
|
||||
import {getItems} from 'mattermost-redux/selectors/entities/items';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import ItemList from './item_list';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
return {
|
||||
items: getItems(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
removeItem,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ItemList);
|
||||
```
|
||||
|
||||
If the selectors and/or actions you need don't yet exist in Redux then you should go add those first by following the [guide to adding actions and selectors](/developers/contribute/more-info/webapp/redux/actions).
|
||||
|
||||
Your `index.ts` and `item_list.ts` files will live together in an `item_list/` directory.
|
||||
|
||||
## Implement the component
|
||||
|
||||
With the props defined and, if necessary, the container built, you're ready to implement the rest of your component. For the most part, implementing a component for the web app is no different than building any other React component. While older code tends to use class components which extend `React.PureComponent`, most newer code should use functional components.
|
||||
|
||||
Our `ItemList` example might look something like this:
|
||||
|
||||
```tsx
|
||||
type Props = {
|
||||
/**
|
||||
* The title of the list
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* An array of item components to display
|
||||
*/
|
||||
items: ListItem[];
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* An action to remove an item from the list
|
||||
*/
|
||||
removeItem: (item: ListItem) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export default function ItemList(props: Props) {
|
||||
const title = this.props.title ? <h1>{this.props.title}</h1> : null;
|
||||
const items = this.props.items.map((item: ListItem) => (
|
||||
<Item
|
||||
key={item.id}
|
||||
item={item}
|
||||
removeItem={this.props.actions.removeItem}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className='item-list'>
|
||||
{title}
|
||||
{items}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
To test your component, [follow the guide here](/developers/contribute/more-info/webapp/unit-testing).
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: "Web app workflow"
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
This page contains most of the information required for a developer to work with the Mattermost web app. Note that everything in this document will refer to working in the `webapp` directory of [the main Mattermost repository](https://github.com/mattermost/mattermost) unless otherwise stated.
|
||||
|
||||
### Workflow
|
||||
|
||||
1. If you haven't done so already, [set up your developer environment](/developers/contribute/developer-setup).
|
||||
|
||||
2. On your fork, create a feature branch for your changes. Name it `MM-$NUMBER_$DESCRIPTION` where `$NUMBER` is the [Jira](https://mattermost.atlassian.net) ticket number you are working on and `$DESCRIPTION` is a short description of your changes. Example branch names are `MM-18150_plugin-panic-log` and `MM-22037_uppercase-email`. You can also use the name `GH-$NUMBER_$DESCRIPTION` for tickets come from [GitHub Issues](https://github.com/mattermost/mattermost/issues).
|
||||
|
||||
3. Make the code changes required to complete your ticket, making sure to write or modify unit tests where appropriate. Use `make test` to run the unit tests.
|
||||
|
||||
4. To run your changes locally, you'll need to run both the client and server. The server and client can either be run together or separately as follows:
|
||||
* You can run both together by using `make run` from the server directory. Both server and web app will be run together and can be stopped by using `make stop`. If you run into problems getting the server running this way, you may want to consider running them separately in case the output from one is hiding errors from the other.
|
||||
|
||||
* You can run the server independently by running `make run-server` from its directory and, using another terminal, you can run the web app by running `make run` from the web app directory. Each can be stopped by running `make stop-server` or `make stop` from their respective directories.
|
||||
|
||||
Once you've done either of those, your server will be available at `http://localhost:8065` by default. Changes to the web app will be built automatically, but changes to the server will only be applied if you restart the server by running `make restart-server` from the server directory.
|
||||
|
||||
5. If you added or changed any translatable text, you will need to update the English translation files to make them available to translators for other languages. You can do that by navigating to `channels` and running `make i18n-extract` to update `src/i18n/en.json`.
|
||||
* Remember to double check that any newly added strings have the correct values in case they weren't detected correctly.
|
||||
* Generally, only `en.json` should be modified directly from this repository. Other languages' translation files are updated using [Weblate](https://translate.mattermost.com).
|
||||
|
||||
6. Before submitting a PR, make sure to check your coding style and run the automated tests on your changes. These are checked automatically by CI, but they should be run manually before submitting changes to ensure the review process goes smoothly.
|
||||
* To check the code style and run the linter, run `make check-style`. If any problems are encountered, they may be able to be automatically fixed by using `make fix-style`.
|
||||
* To run the type checker, use `make check-types`.
|
||||
* To run the unit tests, run `make test`.
|
||||
|
||||
7. Commit your changes, push your branch and [create a pull request](https://developers.mattermost.com/blog/submitting-great-prs/).
|
||||
|
||||
8. Respond to feedback on your pull request and make changes as necessary by committing to your branch and pushing it. Your branch should be kept roughly up to date by [merging](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging#_basic_merging) master into it periodically. This can either be done using [`git merge`](https://git-scm.com/docs/git-merge) or, as long as there are no conflicts, by commenting `/update-branch` on the PR.
|
||||
|
||||
9. That's it! Rejoice that you've helped make Mattermost better.
|
||||
|
||||
### Useful Mattermost commands
|
||||
|
||||
During development you may want to reset the database and generate random data for testing your changes. See [the corresponding section of the server developer workflow](/developers/contribute/more-info/server/developer-workflow#useful-mattermost-commands) for how to do that.
|
||||
161
docs/develop/contribute/more-info/webapp/e2e-cheatsheets.md
Normal file
161
docs/develop/contribute/more-info/webapp/e2e-cheatsheets.md
Normal file
@@ -0,0 +1,161 @@
|
||||
---
|
||||
title: "End-to-End (E2E) cheatsheets"
|
||||
sidebar_position: 7
|
||||
---
|
||||
|
||||
This page compiles all Cypress custom commands based on specific sections of the web app, as well as other general examples. The examples provided showcase the best and conventions on how to write great automated test scripts.
|
||||
We encourage everyone to read this information, ask questions if something is not clear, and challenge the mentioned practices so that we can continuously refine and improve.
|
||||
|
||||
If you need to add more custom commands, add them to `/e2e-tests/cypress/tests/support`, and check out the [Cypress custom commands](https://docs.cypress.io/api/cypress-api/custom-commands.html) documentation. For ease of use, in-code documentation functionality, and making custom commands more discoverable, add type definitions. See this example of a [declaration file](https://github.com/mattermost/mattermost/blob/master/e2e-tests/cypress/tests/support/api/user.d.ts) for reference on how to include and make type definitions.
|
||||
_____
|
||||
### General Queries with the Testing Library
|
||||
|
||||
The [Testing Library](https://testing-library.com/) is used through the package `@testing-library/cypress`, and it provides simple and complete custom Cypress commands and utilities that encourage such good testing practices. To decide on the queries from the Testing Library you should be using while writing Cypress tests, check out this [article](https://testing-library.com/docs/guide-which-query/) to learn more. For instance, you can select something with test ID using: `cy.findByTestId`.
|
||||
|
||||
If you need more help, check out the [online Testing Playground](https://testing-playground.com/)—no install required, always up-to-date, and it teaches you the exact Testing-Library queries you should be using as you click through the DOM. And if you’d rather stay in a VSCode editor, check out the [VS Code Testing Playground](https://marketplace.visualstudio.com/items?itemName=aganglada.vscode-testing-playground) extension, which brings the same interactive query suggestions and best-practice guidance right into your IDE.
|
||||
|
||||
The following is a short summary of the recommended order of priority for queries:
|
||||
|
||||
#### :white_check_mark: Queries Accessible to Everyone
|
||||
These reflect the experience of visual/mouse users as well as those that use assistive technology. Examples include: `cy.findByRole`, `cy.findByLabelText`, `cy.findByPlaceholderText`, `cy.findByText`, and `cy.findByDisplayValue`.
|
||||
|
||||
#### :white_check_mark: Semantic Queries
|
||||
These use HTML5 and ARIA–compliant selectors. Note that the user experience of interacting with these attributes varies greatly across browsers and assistive technology. Some examples include: `cy.findByAltText` and `cy.findByTitle`.
|
||||
|
||||
#### :warning: Base Queries
|
||||
These are considered part of implementation details and are discouraged to be used. You will still find base queries in the codebase but they will be replaced soon. Therefore, please refrain from reusing the existing base query patterns. However, you may want to use them only to limit the scope of selection. Examples include: `cy.get('#elementId')` and `cy.get('.class-name')`. Below is an acceptable use case of base queries:
|
||||
|
||||
```javascript
|
||||
// limit the scope but chained with recommended query
|
||||
cy.get('#elementId').should('be.visible').findByRole('button', {name: 'Save'}).click();
|
||||
|
||||
// limit the scope then use the recommended queries within the scope
|
||||
cy.get('.class-name').should('be.visible').within(() => {
|
||||
cy.findByRole('input', {name: 'Position'}).type('Software Developer');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
});
|
||||
```
|
||||
|
||||
#### :white_check_mark: Query Variants
|
||||
|
||||
Note that `cy.findBy*` are shown but other variants are `cy.findAllBy*`, `cy.queryBy*`, and `cy.queryAllBy*`. See the [Queries](https://testing-library.com/docs/dom-testing-library/api-queries) section from `testing-library`.
|
||||
|
||||
#### :x: Off-limits Queries
|
||||
Please do not use any `Xpath` selectors such as the descendant selector. Do not use the `ul > li` and order selectors either, like `ul > li:nth-child(2)`. If an element can only be queried with this approach, then you may modify the application codebase, improve it, and make it "accessible to everyone".
|
||||
_____
|
||||
### Settings Modal
|
||||

|
||||
|
||||
#### Opening the settings modal
|
||||
The function `cy.uiOpenSettingsModal(section)` opens the settings modal when viewing a channel. `section` is of the
|
||||
< <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type">string</a> > type. Possible values for `section` are: `'Notifications'`, `'Display'`, `'Sidebar'`, and `'Advanced'`.
|
||||
|
||||
* **Open 'Settings' modal and view the default 'General Settings'**: `cy.uiOpenSettingsModal();`
|
||||
* **Open the Settings modal and view a specific section (like the 'Advanced' section)**: `cy.uiOpenSettingsModal('Advanced');`
|
||||
* **Open the Settings modal, view a specific section, and change a setting**:
|
||||
```javascript
|
||||
// # Open 'Advanced' section of 'Settings' modal
|
||||
cy.uiOpenSettingsModal('Advanced').within(() => {
|
||||
// # Open 'Enable Join/Leave Messages' and turn it off
|
||||
cy.findByRole('heading', {name: 'Enable Join/Leave Messages'}).click();
|
||||
cy.findByRole('radio', {name: 'Off'}).click();
|
||||
// # Save and close the modal
|
||||
cy.uiSave();
|
||||
cy.uiClose();
|
||||
});
|
||||
```
|
||||
|
||||
#### Selecting a section's button within a modal
|
||||
Use the function `cy.findByRoleExtended('button', {name})`. `name` is of the < <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type">string</a> > type. Possible values for `name` are: `'Notifications'`, `'Display'`, `'Sidebar'`, and `'Advanced'`.
|
||||
|
||||
* **Clicking a button within the Settings modal**:
|
||||
```javascript
|
||||
// # Open 'Advanced' section of 'Settings' modal
|
||||
cy.uiOpenSettingsModal().within(() => {
|
||||
// # Click 'Notifications' button
|
||||
cy.findByRoleExtended('button', {name: 'Notifications'}).should('be.visible').click();
|
||||
});
|
||||
```
|
||||
#### Select a section's setting within a modal via the name of the section
|
||||
Use the function `cy.findByRole('heading', {name})`. `name` is of the < <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type">string</a> > type. Possible values for `name` are: `'Full Name'`, `'Username'`, and others depending on the sections in the modal.
|
||||
|
||||
* **Open a section within the Settings modal**:
|
||||
```javascript
|
||||
// # Open 'Notifications' of 'Settings' modal
|
||||
cy.uiOpenSettingsModal('Notifications').within(() => {
|
||||
// # Open 'Words That Trigger Mentions' setting
|
||||
cy.findByRole('heading', {name: 'Words That Trigger Mentions'}).should('be.visible').click();
|
||||
});
|
||||
```
|
||||
|
||||
#### Select a section's setting within a modal via role
|
||||
Use the function `cy.findByRole(role, {name})`. `role` is of the < <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type">string</a> > type. Possible values for `role` are: `'textbox'`, `'radio'`, `'checkbox'` and other <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles">roles</a>. `name` is of the < <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type">string</a> > type. Possible values for `name` are: `'On'`, `'Off'`, and others depending on a section's settings.
|
||||
* **Change value of a section's setting in the Settings modal**:
|
||||
```javascript
|
||||
// # Open 'Notifications' of 'Settings' modal
|
||||
cy.uiOpenSettingsModal('Notifications').within(() => {
|
||||
// # Open 'Words That Trigger Mentions' setting
|
||||
cy.findByRole('heading', {name: 'Words That Trigger Mentions'}).should('be.visible').click();
|
||||
// # Check channel-wide mentions
|
||||
cy.findByRole('checkbox', {name: 'Channel-wide mentions "@channel", "@all", "@here"'}).click();
|
||||
});
|
||||
```
|
||||
|
||||
#### Saving and closing a modal
|
||||
`cy.uiSave` and `cy.uiClose` are common functions that can be used to save things and close modals.
|
||||
|
||||
* **Saving and closing in the Settings modal**:
|
||||
```javascript
|
||||
// # Open 'Notifications' of 'Settings' modal
|
||||
cy.uiOpenSettingsModal('Notifications').within(() => {
|
||||
// # Open 'Words That Trigger Mentions' setting
|
||||
cy.findByRole('heading', {name: 'Words That Trigger Mentions'}).should('be.visible').click();
|
||||
// # Check channel-wide mentions
|
||||
cy.findByRole('checkbox', {name: 'Channel-wide mentions "@channel", "@all", "@here"'}).click();
|
||||
// # Save then close the modal
|
||||
cy.uiSave();
|
||||
cy.uiClose();
|
||||
});
|
||||
```
|
||||
_____
|
||||
### Channel Menu
|
||||

|
||||
|
||||
#### Opening the channel menu
|
||||
Use the function `cy.uiOpenChannelMenu(item)`. This will open the channel menu by clicking the channel header title or dropdown icon when viewing a channel. `item` is of the type < <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type">string</a> >. Possible values for `item` are: `'View Info'`, `'Move to...'`,`'Notification Preferences'`, `'Mute Channel'`, `'Add Members'`, `'Manage Members'`,`'Edit Channel Header'`, `'Edit Channel Purpose'`, `'Rename Channel'`, and `'Convert to Private Channel'`, `'Archive Channel'`, and `'Leave Channel'`.
|
||||
|
||||
* **Open the channel menu normally**:
|
||||
```javascript
|
||||
// # Open 'Channel Menu'
|
||||
cy.uiOpenChannelMenu();
|
||||
```
|
||||
* **Open the channel menu and click on a specific item**:
|
||||
```javascript
|
||||
// # Open 'Advanced' section of 'Settings' modal
|
||||
cy.uiOpenChannelMenu('View Info');
|
||||
```
|
||||
#### Closing the channel menu
|
||||
Use the function `cy.uiCloseChannelMenu()`. This will close the channel menu by clicking the channel header title or dropdown icon again at the center channel view, given that the menu is already open.
|
||||
|
||||
#### Get the DOM elements of the channel menu
|
||||
Use the function `cy.uiGetChannelMenu()`.
|
||||
_____
|
||||
### Product Menu
|
||||

|
||||
|
||||
#### Opening the product menu
|
||||
Use the function `cy.uiOpenProductMenu(item)`. `item` is of the type < <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type">string</a> >. Possible values for `item` are: `'Channels'`, `'Boards'`, `'Playbooks'`, `'System Console'`, `'Integrations'`, `'Marketplace'`, `'Download Apps'`, and `'About Mattermost'`.
|
||||
|
||||
* **Open the product menu normally**:
|
||||
```javascript
|
||||
// # Open 'Product menu'
|
||||
cy.uiOpenProductMenu();
|
||||
```
|
||||
* **Open the product menu and click on a specific item**:
|
||||
```javascript
|
||||
// # Open 'Integrations' section of 'Product Menu' modal
|
||||
cy.uiOpenProductMenu('Integrations');
|
||||
```
|
||||
#### Get the DOM elements of the product menu
|
||||
Use the function `cy.uiGetProductMenu()`.
|
||||
_____
|
||||
300
docs/develop/contribute/more-info/webapp/e2e-testing.md
Normal file
300
docs/develop/contribute/more-info/webapp/e2e-testing.md
Normal file
@@ -0,0 +1,300 @@
|
||||
---
|
||||
title: "End-to-End (E2E) tests"
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
End-to-end tests for the Mattermost web app in general use [Cypress](https://www.cypress.io/) and [Playwright](https://playwright.dev/). If you're not familiar with Cypress, check out the Cypress [Developer Guide](https://docs.cypress.io/guides/overview/why-cypress.html#In-a-nutshell) and [API Reference](https://docs.cypress.io/api/api/table-of-contents.html). Feel free to also join us on the Mattermost Community server if you'd like to ask questions and collaborate with us!
|
||||
|
||||
<Note title="NOTE">
|
||||
Playwright is a new framework getting added to the Mattermost web app for test automation (and is currently being used for visual tests). Documentation about Playwright in the web app is in development, so all other content about E2E testing will be related to Cypress.
|
||||
|
||||
If you're looking for information related to E2E tests and Redux, please check out [Redux Unit and E2E Testing](/developers/contribute/more-info/webapp/redux/testing).
|
||||
</Note>
|
||||
|
||||
|
||||
### What requires an E2E test?
|
||||
|
||||
* Test cases that are defined in [help-wanted E2E issues](https://github.com/mattermost/mattermost/issues?q=label%3A%22Area%2FE2E+Tests%22+label%3A%22Help+Wanted%22+is%3Aopen+is%3Aissue+).
|
||||
* New features and stories - For example, check out [MM-19922 Add E2E tests for Mark as Unread #4243](https://github.com/mattermost/mattermost-webapp/pull/4243) which contains E2E tests for the `Mark As Unread` feature.
|
||||
* Bug fixes - For example, see [MM-26751: Fix highlighting of at-mentions of self #5908](https://github.com/mattermost/mattermost-webapp/pull/5908), which fixes a highlighting issue and adds a related test.
|
||||
* Test cases from [Zephyr](https://support.smartbear.com/zephyr-scale-cloud/docs/) - For example, see [Added Cypress tests MM-T1410, MM-T1415 and MM-T1419 #5850](https://github.com/mattermost/mattermost-webapp/pull/5850) which adds automated tests for `Guest Accounts`.
|
||||
|
||||
### File Structure for E2E Testing
|
||||
E2E tests are located at the root of the repository in [the `e2e-tests` folder](https://github.com/mattermost/mattermost/tree/master/e2e-tests). The file structure is mostly based on the [Cypress scaffold](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests#Folder-Structure). Here is an overview of some important folders and files:
|
||||
|
||||
```
|
||||
|-- e2e-tests
|
||||
|-- cypress
|
||||
|-- tests
|
||||
|-- fixtures
|
||||
|-- integration
|
||||
|-- plugins
|
||||
|-- support
|
||||
|-- utils
|
||||
|-- cypress.config.ts
|
||||
|-- package.json
|
||||
```
|
||||
|
||||
* `/e2e-tests/cypress/tests/fixtures` or [Fixture Files](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Fixture-Files):
|
||||
- Fixtures are used as external pieces of static data that can be used by tests.
|
||||
- Typically used with the `cy.fixture()` command and most often when stubbing network requests.
|
||||
* `/e2e-tests/cypress/tests/integration` or [Test Files](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Test-files):
|
||||
- Subfolder naming convention depends on test grouping, which is usually based on the general functional area (e.g. `/e2e/cypress/tests/integration/messaging/` for "Messaging").
|
||||
* `/e2e-tests/cypress/tests/plugins` or [Plugin Files](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Plugins-file):
|
||||
- A convenience mechanism that automatically includes plugins before running every single `spec` file.
|
||||
* `/e2e-tests/cypress/tests/support` or [Support Files](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Support-file):
|
||||
- A support file is a place for reusable behaviour such as custom commands or global overrides that are available and can be applied to all `spec` files.
|
||||
* `/e2e-tests/cypress/tests/utils`: this folder contains common utility functions.
|
||||
* `/e2e-tests/cypress/cypress.config.ts`: this file is for Cypress [configuration](https://docs.cypress.io/guides/references/configuration.html#Options).
|
||||
* `/e2e-tests/cypress/package.json`: this file is for all the dependencies related to Cypress end-to-end testing.
|
||||
|
||||
### Writing End-to-End Tests
|
||||
|
||||
#### Where should a new test go?
|
||||
You will need to either add the new test to an existing `spec` file, or create a new file. Sometimes, you will be informed (for example through issue descriptions) of the specific folder the test file should go in, or the actual test file being amended. As aforementioned, the `e2e-tests/cypress/tests/integration` folder is where all of the tests live, with subdirectories that roughly divide the tests by functional areas. Cypress is configured to look for and run tests that match the pattern of `*_spec.ts`, so a good new test file name for an issue like [Write Web App E2E with Cypress: "MM-T642 Attachment does not collapse" #18184](https://github.com/mattermost/mattermost/issues/18184) would be `attachment_does_not_collapse_spec.ts`, to ensure that it gets picked up.
|
||||
> *Note*: There may be some JavaScript `spec` files, but new tests should be written in TypeScript. If you are adding a test to an existing `spec` file, convert that file to TypeScript if necessary.
|
||||
|
||||
If you don't know where a test should go, first check the names of the subdirectories, and select a folder that describes the functional area of the test best. From there, look to see if there is already a `spec` file that may be similar to what you are testing; if there is one, it would be possible to add the test to the pre-existing file.
|
||||
|
||||
#### Test metadata on spec files
|
||||
Test metadata is used to identify each `spec` file before it is forwarded for a Cypress run, and the metadata is located at the start of a `spec` file. Currently, supported test metadata fields include the following:
|
||||
|
||||
* **Stage** - Indicates the environment for testing; valid values for this include `@prod`, `@smoke`, `@pull_request`. "Stage" metadata in `spec` files are owned and controlled by the Quality Assurance (QA) team who carefully analyze the stability of tests and promote/demote them into certain stages. This is not required when submitting a `spec` file and it should be removed when modifying an existing `spec` file.
|
||||
|
||||
* **Group** - Indicates test group or category, which is primarily based on functional areas and existing release testing groups. Valid values for this include: `@settings` for Settings, `@playbooks` for Playbooks, etc. This is required when submitting a `spec` file.
|
||||
|
||||
* **Skip** - This is a way to skip running a `spec` file depending on the capabilities of the test environment. This is required when submitting a `spec` file if there is a test that has certain limitations or requirements. Forms of capabilities include:
|
||||
- **Platform-related**: valid values include - `@darwin` for Mac, `@linux` for Linux flavors like Ubuntu, `@win32` for Windows, etc.
|
||||
- **Browser-related**: valid values include - `@electron`, `@chrome`, `@firefox`, `@edge`, etc.
|
||||
- **User interface-related**: valid values include `@headless` or `@headed`.
|
||||
|
||||
A `spec` file can have zero or more metadata values separated by spaces (for example, `// Stage: @prod @smoke`). A more full example of what metadata would look like at the start of a `spec` file (for example, `attachment_does_not_collapse_spec.ts`) would be:
|
||||
|
||||
```
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// ***************************************************************
|
||||
// - [#] indicates a test step (e.g. # Go to a page)
|
||||
// - [*] indicates an assertion (e.g. * Check the title)
|
||||
// - Use element ID when selecting an element. Create one if none.
|
||||
// ***************************************************************
|
||||
|
||||
// Stage: @prod
|
||||
// Group: @incoming_webhook
|
||||
```
|
||||
|
||||
The metadata is part of a comment block that also includes information on copyright and license, and a section to explain how to tag comments in your code appropriately.
|
||||
|
||||
#### Setting up test code
|
||||
|
||||
Underneath the comment header, we can add the starter code as defined from the "Test code arrangement" part of the issue. Each test (no matter the situation you're writing a test for) should have a corresponding test case in Zephyr. Therefore, the `describe` block encompassing the test code should correspond to folder name in Zephyr (e.g. "Incoming webhook"), and the `it` block should contain `Zephyr test case number` as `Test Key`, and then the test title. For [Write Web App E2E with Cypress: "MM-T642 Attachment does not collapse" #18184](https://github.com/mattermost/mattermost/issues/18184), in the spec file made for it (`attachment_does_not_collapse_spec.ts`), the starter code would be:
|
||||
```javascript
|
||||
describe('Integrations/Incoming Webhook', () => {
|
||||
it('MM-T642 Attachment does not collapse', () => {
|
||||
// Put test steps and assertions here
|
||||
});
|
||||
});
|
||||
```
|
||||
For those writing E2E from Help Wanted tickets with `Area/E2E Tests` label, the `Test Key` is available in the Github issue itself. The `Test Key` is used for mapping test cases per Release Testing specification. It will be used to measure coverage between manual and automated tests. In case the `Test Key` is not available, feel free to prompt the QA team who will either search for an existing Zephyr entry or if it's a new one, it will be created for you.
|
||||
|
||||
#### Using Cypress Hooks
|
||||
|
||||
Before writing the main body of the test in the `it` block, it can help to write some setup code for test isolation using [hooks](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests#Hooks). In a `before()` hook, you can run tests in isolation using the custom command `cy.apiInitSetup()`. This command creates a new team, channel, and user which can only be used by the spec file itself. Make use of the `cy.apiInitSetup()` function as much as possible, as it is recommended to log in as a new user and visit the generated team and/or channel. Avoid the use of `sysadmin` user or default `ad-1` team if possible.
|
||||
|
||||
For `attachment_does_not_collapse_spec.ts` for example:
|
||||
```javascript
|
||||
let incomingWebhook;
|
||||
let testChannel;
|
||||
|
||||
before(() => {
|
||||
// # Create and visit new channel and create incoming webhook
|
||||
cy.apiInitSetup().then(({team, channel}) => {
|
||||
testChannel = channel;
|
||||
|
||||
const newIncomingHook = {
|
||||
channel_id: channel.id,
|
||||
channel_locked: true,
|
||||
description: 'Incoming webhook - attachment does not collapse',
|
||||
display_name: 'attachment-does-not-collapse',
|
||||
};
|
||||
|
||||
cy.apiCreateWebhook(newIncomingHook).then((hook) => {
|
||||
incomingWebhook = hook;
|
||||
});
|
||||
|
||||
cy.visit(`/${team.name}/channels/${channel.name}`);
|
||||
});
|
||||
});
|
||||
```
|
||||
The `before()` hook is also a good place to add checks if a test requires a certain kind of server license. If test(s) require a certain licensed feature, use the function `cy.apiRequireLicenseForFeature('<feature name>')`. To check if the server has a license in general, use `cy.apiRequireLicense()`. You can also add hard requirements in the `before()` hook, such as: `cy.shouldNotRunOnCloudEdition()`, `cy.shouldRunOnTeamEdition()`, `cy.shouldHavePluginUploadEnabled()`, `cy.shouldHaveElasticsearchDisabled()`, and `cy.requireWebhookServer()`. For more information on custom commands and how to select elements, check out the [End-to-End (E2E) cheatsheets](/developers/contribute/more-info/webapp/e2e-cheatsheets).
|
||||
|
||||
Putting what you've gone through so far all together, you should have code that looks similar to this template:
|
||||
|
||||
```javascript
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// **********************************************************************
|
||||
// - Use [#] in comment to indicate a test step (e.g. # Go to a page)
|
||||
// - Use [*] in comment to indicate an assertion (e.g. * Check the title)
|
||||
// - Query an element with @testing-library/cypress as much as possible
|
||||
// **********************************************************************
|
||||
|
||||
// Group: @change_group
|
||||
|
||||
describe('Change to Functional Group', () => {
|
||||
before(() => {
|
||||
// Add hard requirement(s) to immediately fail and throw a descriptive error if not met
|
||||
// cy.shouldNotRunOnCloudEdition();
|
||||
|
||||
// Add license requirement(s)
|
||||
// cy.apiRequireLicense();
|
||||
|
||||
// Init basic setup for test isolation
|
||||
cy.apiInitSetup({loginAfter: true}).then(({team, channel, user}) => {
|
||||
// Assign return values to variable/s
|
||||
// # Visit a channel
|
||||
// Do other setup per test data preconditions
|
||||
});
|
||||
});
|
||||
|
||||
// Add a title of "[Zephyr_id] - [Zephyr title]" for test case with single step,
|
||||
// or "[Zephyr_id]_[step_number] - [Zephyr title]" for test case with multiple steps
|
||||
it('[Zephyr_id] - [Zephyr title]', () => {
|
||||
// Put test steps and assertions here
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### Main body of the test
|
||||
|
||||
|
||||
<Note title="NOTE">
|
||||
Use `camelCase` when assigning to `data-testid` or element ID. Also, watch out for potential breaking changes in the snapshot from [unit testing](/developers/contribute/more-info/webapp/unit-testing). Run `make test` to see if all unit tests are passing, and run `npm run updatesnapshot` or `npm run test -- -u` if necessary to update snapshot tests.
|
||||
</Note>
|
||||
|
||||
|
||||
Now, inside the body of the `it` block , we will write in code the "Steps" part of the E2E issue. The following steps and code are from [Write Web App E2E with Cypress: "MM-T642 Attachment does not collapse" #18184](https://github.com/mattermost/mattermost/issues/18184). Check out the complete file at: [`attachment_does_not_collapse_spec.ts`](https://github.com/mattermost/mattermost-webapp/pull/11231/files).
|
||||
|
||||
* **Create an incoming webhook and send it through POST with attachment**:
|
||||
```javascript
|
||||
// # Post the incoming webhook with a text attachment
|
||||
const content = '[very long lorem ipsum test text]';
|
||||
const payload = {
|
||||
channel: testChannel.name,
|
||||
attachments: [{fallback: 'testing attachment does not collapse', pretext: 'testing attachment does not collapse', text: content}],
|
||||
};
|
||||
cy.postIncomingWebhook({url: incomingWebhook.url, data: payload, waitFor: 'attachment-pretext'});
|
||||
```
|
||||
* **View the webhook post that has the attachment**: you are already in the channel that has the attachment post, as specified by the line `cy.visit('/${team.name}/channels/${channel.name}')`; from the setup section of the code.
|
||||
|
||||
* **Type /collapse and press Enter**:
|
||||
```javascript
|
||||
// * Check "show more" button is visible and click
|
||||
cy.getLastPostId().then((postId) => {
|
||||
const postMessageId = `#${postId}_message`;
|
||||
cy.get(postMessageId).within(() => {
|
||||
cy.get('#showMoreButton').scrollIntoView().should('be.visible').and('have.text', 'Show more').click();
|
||||
});
|
||||
});
|
||||
// # Type /collapse and press Enter
|
||||
const collapseCommand = 'collapse';
|
||||
cy.uiGetPostTextBox().type(`/${collapseCommand} {enter}`);
|
||||
```
|
||||
|
||||
* **Observe the integration post with the Message Attachment**: where you ascertain what is expected of the test.
|
||||
```javascript
|
||||
cy.getNthPostId(-2).then((postId) => {
|
||||
const postMessageId = `#${postId}_message`;
|
||||
cy.get(postMessageId).within(() => {
|
||||
// * Verify "show more" button says "Show less"
|
||||
cy.get('#showMoreButton').scrollIntoView().should('be.visible').and('have.text', 'Show less');
|
||||
// * Verify gradient
|
||||
cy.get('#collapseGradient').should('not.be.visible');
|
||||
});
|
||||
```
|
||||
|
||||
### Running E2E Tests
|
||||
#### On your local development machine / Gitpod
|
||||
|
||||
1. If the server is not running, launch it by running `make run` in the `server` directory. Then, confirm that the Mattermost instance has started successfully. You can also run `make test-data` in the `server` directory to preload your server instance with initial seed data (you may need to restart the server again).
|
||||
- Each test case should handle the required system or user settings, but if you encounter an unexpected error while testing, you may want to reset the configuration of the server to the default by going to the `server` directory and running `make config-reset`.
|
||||
2. Change the directory to `e2e-tests/cypress`, and install dependencies by running `npm i`.
|
||||
3. You can then run tests in a variety of different ways by using the following commands in the `e2e-tests/cypress` directory:
|
||||
|
||||
- **Running all E2E tests**: `npm run cypress:run`. This does not include the `spec` files in the `/e2e-tests/cypress/tests/integration/enterprise` folder because they need an Enterprise license to run successfully.
|
||||
- **Running tests selectively based on `spec` metadata**: For example, if you want to run all the tests in a specific group, such as those in "accessibility", the command would be: `node run_tests.js --group='@accessibility'`.
|
||||
- **Using the Cypress desktop app**: `npm run cypress:open`. This will start up the Cypress desktop app, where you will be able to do partial testing depending on the `spec` selected in the app. If you are using Gitpod, the Cypress app will open up in the VNC desktop, which is accessible at port `6080`.
|
||||
4. Don't forget to check your coding styles! See the [Web app workflow](/developers/contribute/more-info/webapp/developer-workflow) page for helpful commands to run.
|
||||
|
||||
#### In a Continuous Integration (CI) pipeline
|
||||
|
||||
All tests are run by Mattermost in a CI pipeline, and they are grouped according to test stability.
|
||||
|
||||
1. __Daily production tests against development branch (master)__: Initiated on the master branch by using the command `node run_tests.js --stage='@prod'`. These production tests are selected and also labeled with `@prod` in the test metadata. See <a target="_blank" href="https://community.mattermost.com/core/pl/g6wx1d84ibdf7r5frjap4rb55a">link</a> for an example test run posted in our community channel.
|
||||
2. __Daily production tests against release branch__: Same as above except the test is initiated against the release branch. See <a target="_blank" href="https://community.mattermost.com/core/pl/8r4f17fkutbxxcwumk5mzwpp5c">link</a> for an example test run.
|
||||
3. __Daily unstable tests against development branch (master)__: Initiated on the master branch by using the command `node run_tests.js --stage='@prod' --invert` to run all tests except production tests. These are called "unstable tests" as they either consistently or intermittently fail due to automation bugs, and not because of product bugs.
|
||||
|
||||
#### Environment variables
|
||||
|
||||
Several environment variables (env variables) are used when testing with Cypress in order to easily change things when running tests in CI and to cater to different values across developer machines.
|
||||
|
||||
Environment variables are [defined in cypress.config.ts](https://github.com/mattermost/mattermost/blob/master/e2e-tests/cypress/cypress.config.ts) under the `env` key. In most cases you don't need to change the values, because it makes use of the default local developer setup. If you do need to make changes, the easiest method is to override by exporting `CYPRESS_*`, where `*` is the key of the variable, for example: `CYPRESS_adminUsername`. See the [Cypress documentation on environment variables](https://docs.cypress.io/guides/guides/environment-variables.html#Setting) for details.
|
||||
|
||||
| Variable | Description |
|
||||
|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| CYPRESS\_adminUsername | Admin's username for the test server.<br/><br/>*Default*: `sysadmin` when server is seeded by `make test-data`. |
|
||||
| CYPRESS\_adminPassword | Admin's password for the test server.<br/><br/>*Default*: `Sys@dmin-sample1` when server is seeded by `make test-data`. |
|
||||
| CYPRESS\_dbClient | The database of the test server. It should match the server config `SqlSettings.DriverName`.<br/><br/>*Default*: `postgres` <br/>*Valid values*: `postgres` or `mysql` |
|
||||
| CYPRESS\_dbConnection | The database connection string of the test server. It should match the server config `SqlSettings.DataSource`.<br/><br/> *Default*: `"postgres://mmuser:mostest_password@localhost/mattermost_test?sslmode=disable\u0026connect_timeout=10"` |
|
||||
| CYPRESS\_enableVisualTest | Use for visual regression testing.<br/><br/>*Default*: `false`<br/>*Valid values*: `true` or `false` |
|
||||
| CYPRESS\_ldapServer | Host of the Lightweight Directory Access Protocol (LDAP) server.<br/><br/>*Default*: `localhost` |
|
||||
| CYPRESS\_ldapPort | Port of the LDAP server.<br/><br/>*Default*: `389` |
|
||||
| CYPRESS\_runLDAPSync | Option to run LDAP sync.<br/><br/>*Default*: `true`<br/>*Valid values*: `true` or `false` |
|
||||
| CYPRESS\_resetBeforeTest | When set to `true`, it deletes all teams and their channels where `sysadmin` is a member except `eligendi` team and its channels.<br/><br/>*Default*: `false`<br/>*Valid values*: `true` or `false` |
|
||||
| CYPRESS\_webhookBaseUrl | A server used for testing webhook integrations.<br/><br/>*Default*: `http://localhost:3000` when initiated with the command `npm run start:webhook` in the `e2e-tests/cypress` directory. |
|
||||
|
||||
### Submitting your pull request (PR)
|
||||
|
||||
Review the [Test Guidelines](/developers/contribute/more-info/getting-started/test-guideline) for details on how to submit your PR
|
||||
|
||||
### Troubleshooting
|
||||
#### Test(s) failing due to a known issue
|
||||
If test(s) are failing due to another known issue, follow these steps to amend your test:
|
||||
1. Append the Jira issue key in the test title, following the format of ` -- KNOWN ISSUE: [Jira_key]`. For example:
|
||||
```javascript
|
||||
describe('Upload Files', () => {
|
||||
it('MM-T2261 Upload SVG and post -- KNOWN ISSUE: MM-38982', () => {
|
||||
// Test steps and assertion here
|
||||
});
|
||||
});
|
||||
```
|
||||
2. Move the test case into a separate `spec` file following the format of `<existing_spec_file_name_[1-9].js>`. For example:
|
||||
`accessibility_account_settings_spec_1.js` and demote the spec file (i.e. remove `// Stage: @prod` from the spec file)
|
||||
3. If all the test cases are failing in a spec file, update each title as mentioned above and demote the spec file.
|
||||
4. Link the failed test case(s) to the Jira issue (the known issue). In the Jira bug, select the **Zephyr Scale** tab. Select the **add an existing one** link, then select test case(s), and finally select **Add**.
|
||||
5. Conversely, remove the Jira issue key if the issue has been resolved and the test is passing.
|
||||
|
||||
#### Cypress failed to start after running `npm run cypress:run`
|
||||
In this problem, either the command line exits immediately without running any test or it logs out like the following with the error message:
|
||||
```sh
|
||||
✖ Verifying Cypress can run /Users/user/Library/Caches/Cypress/3.1.3/Cypress.app
|
||||
→ Cypress Version: 3.1.3
|
||||
Cypress failed to start.
|
||||
|
||||
This is usually caused by a missing library or dependency.
|
||||
```
|
||||
The solution to this problem is to clear node options by initiating `unset NODE_OPTIONS` in the command line. Running `npm run cypress:run` should then proceed with Cypress testing.
|
||||
|
||||
#### Running any Cypress spec gives `ENOSPC`
|
||||
|
||||
This error may occur in Ubuntu when running any Cypress spec:
|
||||
|
||||
```
|
||||
code: 'ENOSPC',
|
||||
errno: 'ENOSPC',
|
||||
```
|
||||
The solution to this problem is to run the following command: `echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p`.
|
||||
|
||||
|
||||
47
docs/develop/contribute/more-info/webapp/index.md
Normal file
47
docs/develop/contribute/more-info/webapp/index.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: "Web app"
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
The Mattermost web app is written in JavaScript using [React](https://react.dev/) and [Redux](https://redux.js.org/).
|
||||
|
||||
## Repository
|
||||
|
||||
It is located in the `webapp` directory of the [main Mattermost repository](https://github.com/mattermost/mattermost).
|
||||
|
||||
https://github.com/mattermost/mattermost/tree/master/webapp
|
||||
|
||||
## Help Wanted
|
||||
|
||||
[Find help wanted tickets here](https://mattermost.com/pl/help-wanted-mattermost-webapp/).
|
||||
|
||||
## Package structure
|
||||
|
||||
The web app is set up as a monorepo which has the code broken up into multiple packages. The main packages in the web app are:
|
||||
|
||||
* `channels` - The main web app which contains Channels, the System Console, login/signup pages, and most of the core infrastructure for the app.
|
||||
* `src/`. Key folders include:
|
||||
* `actions` - Contains Redux actions which make up much of the view logic for the web app
|
||||
* `components` - Contains UI components and views written using React
|
||||
* `i18n` - Contains the localization files for the web app
|
||||
* `packages/mattermost-redux` - Contains most of the Redux logic used for handling data from the server
|
||||
* `plugins` - Contains the plugin framework, utility functions and components
|
||||
* `reducers` - Contains Redux reducers used for view state
|
||||
* `selectors` - Contains Redux selectors used for view state
|
||||
* `tests` - Contains setup code and mocks used for unit testing
|
||||
* `utils` - Contains many widely-used utility functions
|
||||
* `platform` - Packages used by the web app and related projects
|
||||
* `client` - The JavaScript client for Mattermost's REST API, available on NPM as [@mattermost/client](https://www.npmjs.com/package/@mattermost/client)
|
||||
* `components` - A work-in-progress package containing UI components designed to be used by different parts of Mattermost
|
||||
* `types` - The TypeScript types used by Mattermost, available on NPM as [@mattermost/types](https://www.npmjs.com/package/@mattermost/types)
|
||||
|
||||
### Important libraries and technologies
|
||||
|
||||
- [React](https://reactjs.org/) - React is a user interface library used for React apps. Its key feature is that it uses a variation of JavaScript called JSX to declaratively define interfaces using HTML-like syntax.
|
||||
- [Redux](https://redux.js.org/) - Redux is a state management library used for JavaScript apps. 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.
|
||||
- [Redux Thunk](https://github.com/reduxjs/redux-thunk) - Redux Thunk is a middleware for Redux that's used to write async actions and logic that interacts more closely with the Redux store.
|
||||
- [React Redux](https://react-redux.js.org/) - React Redux is the library used to connect React components to a Redux store.
|
||||
|
||||
## Legacy Notes
|
||||
|
||||
Note that the webapp was previously located at https://github.com/mattermost/mattermost-webapp/. You may find additional history in this repository that was not migrated back to https://github.com/mattermost/mattermost when forming the monorepo.
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: "Migrate to Typescript"
|
||||
sidebar_position: 9
|
||||
---
|
||||
|
||||
The Mattermost team wants to proactively improve the quality, security, and stability of [the code](https://github.com/mattermost/mattermost/tree/master/webapp), and one way to do this is by introducing the usage of type checking. Thus, we have decided to introduce Typescript in our codebase as it's a mature and feature-rich approach.
|
||||
|
||||
As a first step, we have migrated the [mattermost-redux](https://github.com/mattermost/mattermost-redux) library to use Typescript, and are now in the process of migrating the [web app](https://github.com/mattermost/mattermost/tree/master/webapp) to use Typescript.
|
||||
|
||||
This campaign will help with the migration by converting files written in Javascript to type-safe files written in Typescript.
|
||||
|
||||
By completing this campaign, we're looking to:
|
||||
|
||||
- Reduce the errors derived from changes.
|
||||
- Increase the consistency of the code.
|
||||
- Ensure a more defensive programming in the code.
|
||||
|
||||
## Contribute
|
||||
|
||||
If you're interested in contributing, please join the [Typescript Migration channel on community.mattermost.com](https://community.mattermost.com/core/channels/typescript-migration). You can also check out the [Contributors](https://community.mattermost.com/core/channels/tickets) channel, where there are several posts mentioning tickets related to this campaign, each containing the hashtag `#typescriptmigration`. You can work on migrating an individual module to Typescript by claiming a ticket that matches [this GitHub issue search](https://github.com/mattermost/mattermost/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22Area%2FTechnical+Debt%22+label%3A%22Up+For+Grabs%22+Migrate+to+Typescript).
|
||||
|
||||
## Component migration steps
|
||||
|
||||
There are a few steps involved with migrating a file to use Typescript. Some of them may not apply to every file and they may change slightly based on the file you're working on. In general, you can follow these steps as a checklist for work that needs to be done on each file.
|
||||
|
||||
1. Every React component's set of `props` needs to be converted to a new type. You can use the component's `propTypes` as a template for what `props` a given component can expect. This conversion to Typescript includes maintaining whether a given `prop` is required or not. An optional property in Typescript is noted by including a question mark at the end of the property's name.
|
||||
2. A component's `state` also needs to be defined using a type. The initial `state` assignment and any call to `setState` will be indicators of what values are present in the component's state.
|
||||
3. Once a component's `props` and `state` (if any) have been converted, you can define the component as `class MyComponent extends React.PureComponent<Props, State>`. You can omit the `State` portion if the component does not have its own `state`.
|
||||
4. Avoid use of the `any` type except in test files. The components themselves should be as well-defined as possible.
|
||||
5. Most objects we used are typed in the [@mattermost/types](https://github.com/mattermost/mattermost/tree/master/webapp/platform/types) library, if you can't find a type you're looking for.
|
||||
6. Check that the types are all correct using the `make check-types` command.
|
||||
|
||||
## Examples
|
||||
|
||||
You can see example pull requests here:
|
||||
|
||||
- https://github.com/mattermost/mattermost/pull/5840
|
||||
- https://github.com/mattermost/mattermost/pull/5244
|
||||
267
docs/develop/contribute/more-info/webapp/redux/actions.md
Normal file
267
docs/develop/contribute/more-info/webapp/redux/actions.md
Normal 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],
|
||||
};
|
||||
}
|
||||
```
|
||||
12
docs/develop/contribute/more-info/webapp/redux/index.md
Normal file
12
docs/develop/contribute/more-info/webapp/redux/index.md
Normal 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`.
|
||||
@@ -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.
|
||||
107
docs/develop/contribute/more-info/webapp/redux/reducers.md
Normal file
107
docs/develop/contribute/more-info/webapp/redux/reducers.md
Normal 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.
|
||||
82
docs/develop/contribute/more-info/webapp/redux/selectors.md
Normal file
82
docs/develop/contribute/more-info/webapp/redux/selectors.md
Normal 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.
|
||||
102
docs/develop/contribute/more-info/webapp/redux/testing.md
Normal file
102
docs/develop/contribute/more-info/webapp/redux/testing.md
Normal 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.
|
||||
237
docs/develop/contribute/more-info/webapp/unit-testing.md
Normal file
237
docs/develop/contribute/more-info/webapp/unit-testing.md
Normal file
@@ -0,0 +1,237 @@
|
||||
---
|
||||
title: "Unit tests"
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
### Unit Tests for Component and Utility Files
|
||||
|
||||
The last required step in building a web app component is to test it. [Jest](https://jestjs.io/en/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) are the main frameworks/testing utilities used in unit testing components and utility files in our web app. Please visit their respective documentation for detailed information on how to get started, best practices, and updates. React Testing Library is used to render and interact with React components in a way that closely mirrors how users interact with them in the browser, while Jest is used to perform snapshot testing against these components.
|
||||
|
||||
With React Testing Library, you can focus on testing the behavior of your components, rather than their implementation details. This means that your tests will be more resilient to changes in your codebase, and will give you more confidence that your application is working as expected.
|
||||
If you need to unit test something related to Redux, please check out [Redux Unit and E2E Testing](/developers/contribute/more-info/webapp/redux/testing).
|
||||
|
||||
#### Writing unit tests
|
||||
Below is a brief guide on how to do component testing:
|
||||
|
||||
1. Use our [testing library helpers](https://github.com/mattermost/mattermost/blob/master/webapp/channels/src/tests/react_testing_utils.tsx) to render a component and its child components. Use `screen` to interact with the rendered component and assert on the expected results. Match snapshots using default or expected props. Note that while using snapshots is convenient, do not rely solely on this for every test case, as changes can be easily overlooked when using the command `jest --updateSnapshot` to update multiple snapshots at once. For example:
|
||||
```javascript
|
||||
import {render, screen, userEvent} from 'tests/react_testing_utils';
|
||||
const baseProps = {
|
||||
active: true,
|
||||
onSubmit: jest.fn(),
|
||||
};
|
||||
|
||||
test('should match snapshot, not send email notifications', () => {
|
||||
// For components that use Redux, React Intl, or Reach Router, you can also use the renderWithFullContext helper
|
||||
const {container} = render(<EmailNotificationSetting {...baseProps}/>);
|
||||
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
|
||||
// Assert on a specific element rendered by the component
|
||||
const submitButton = screen.getByRole('button', { name: 'Submit' });
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
2. Use screen from React Testing Library to query for elements by text or role and use assertions to verify their existence and properties.
|
||||
```javascript
|
||||
expect(screen.getByRole('checkbox', { id: 'emailNotificationImmediately' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: props.siteName })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: props.customDescriptionText })).toBeInTheDocument();
|
||||
```
|
||||
3. Use toHaveClass matcher from Jest DOM to check CSS classes.
|
||||
```javascript
|
||||
expect(screen.getByTestId('create_post')).toHaveClass('center');
|
||||
```
|
||||
4. Simulate events using userEvent and verify state changes using expect.
|
||||
```javascript
|
||||
test('should pass handleChange', () => {
|
||||
const {getByRole} = render(<EmailNotificationSetting {...baseProps}/>);
|
||||
const emailNotificationImmediately = getByRole('checkbox', { id: 'emailNotificationImmediately' });
|
||||
|
||||
userEvent.click(emailNotificationImmediately);
|
||||
|
||||
expect(emailNotificationImmediately.checked).toBe(true);
|
||||
expect(screen.getByTestId('emailInterval').value).toBe('30');
|
||||
});
|
||||
```
|
||||
5. Ensure that all functions of a component are tested. This can be done via events, state changes, or just calling it directly.
|
||||
```javascript
|
||||
const baseProps = {
|
||||
updateSection: jest.fn(),
|
||||
};
|
||||
|
||||
test('should call updateSection on handleExpand', () => {
|
||||
const {getByRole} = render(<EmailNotificationSetting {...baseProps}/>);
|
||||
const button = getByRole('button', { name: /expand/i });
|
||||
|
||||
userEvent.click(button);
|
||||
|
||||
expect(baseProps.updateSection).toBeCalled();
|
||||
expect(baseProps.updateSection).toHaveBeenCalledTimes(1);
|
||||
expect(baseProps.updateSection).toBeCalledWith('email');
|
||||
});
|
||||
```
|
||||
|
||||
6. When a function is passed to a component via props, make sure to test if it gets called for a particular event call or its state changes.
|
||||
```javascript
|
||||
const baseProps = {
|
||||
onSubmit: jest.fn(),
|
||||
updateSection: jest.fn(),
|
||||
};
|
||||
|
||||
test('should call functions on handleSubmit', () => {
|
||||
const {getByTestId} = render(<EmailNotificationSetting {...baseProps}/>);
|
||||
const submitButton = getByTestId('submit-button');
|
||||
|
||||
userEvent.click(submitButton);
|
||||
|
||||
expect(baseProps.onSubmit).not.toBeCalled();
|
||||
expect(baseProps.updateSection).toHaveBeenCalledTimes(1);
|
||||
expect(baseProps.updateSection).toBeCalledWith('email');
|
||||
|
||||
const emailNotificationNever = getByTestId('email-notification-never');
|
||||
userEvent.change(emailNotificationNever);
|
||||
|
||||
userEvent.click(submitButton);
|
||||
|
||||
expect(baseProps.onSubmit).toBeCalled();
|
||||
expect(baseProps.onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(baseProps.onSubmit).toBeCalledWith({enableEmail: 'false'});
|
||||
|
||||
expect(baseProps.updateSection).toHaveBeenCalledTimes(2);
|
||||
expect(baseProps.updateSection).toBeCalledWith('');
|
||||
});
|
||||
```
|
||||
7. Provide a mock for a single function imported from another file while keeping the original version of the rest of that file's exports.
|
||||
```javascript
|
||||
jest.mock('utils/utils', () => {
|
||||
const original = jest.requireActual('utils/utils');
|
||||
return {
|
||||
...original,
|
||||
isMobileView: jest.fn(() => true),
|
||||
};
|
||||
});
|
||||
```
|
||||
8. Mock async ``redux`` actions as necessary while providing a readable action type and having them pass their arguments.
|
||||
```javascript
|
||||
jest.mock('mattermost-redux/actions/channels', () => {
|
||||
const original = jest.requireActual('mattermost-redux/actions/channels');
|
||||
return {
|
||||
...original,
|
||||
fetchMyChannelsAndMembers: (...args) => ({type: 'MOCK_FETCH_CHANNELS_AND_MEMBERS', args}),
|
||||
};
|
||||
});
|
||||
|
||||
// Then compare the dispatched actions
|
||||
await testStore.dispatch(Actions.loadChannelsForCurrentUser());
|
||||
expect(testStore.getActions()).toEqual(expectedActions);
|
||||
```
|
||||
9. For utility functions, list all test cases with test description, input and output.
|
||||
```javascript
|
||||
describe('stripMarkdown | RemoveMarkdown', () => {
|
||||
const testCases = [{
|
||||
description: 'emoji: same',
|
||||
inputText: 'Hey :smile: :+1: :)',
|
||||
outputText: 'Hey :smile: :+1: :)',
|
||||
},
|
||||
{
|
||||
description: 'at-mention: same',
|
||||
inputText: 'Hey @user and @test',
|
||||
outputText: 'Hey @user and @test',
|
||||
}];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
test(testCase.description, () => {
|
||||
expect(stripMarkdown(testCase.inputText)).toEqual(testCase.outputText);
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
#### Running unit tests
|
||||
10. To run all tests that have been modified or added since the last commit, run [Jest](https://jestjs.io/en/) in `watch` mode with the command: `npm run test:watch`.
|
||||
* If there are new tests in the first run, test snapshots will be generated and placed in the `__snapshots__` directory in the folder where the test suite is located.
|
||||
* In watch mode, you can also run specific sets of tests:
|
||||
|
||||
```
|
||||
› Press a to run all tests.
|
||||
› Press f to run only failed tests.
|
||||
› Press o to only run tests related to changed files.
|
||||
› Press q to quit watch mode.
|
||||
› Press p to filter by a filename regex pattern.
|
||||
› Press t to filter by a test name regex pattern.
|
||||
› Press Enter to trigger a test run.
|
||||
```
|
||||
* To see if a component test passes, select `p` in `watch` mode and enter the filename for the component test.
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
* If you get an error similar to `UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'filter' of undefined`:
|
||||
|
||||
- Check if the code being tested uses native timer functions (i.e., `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`). You can mock the timers and/or run fake timers (e.g. `jest.useFakeTimers()`) if necessary. Note that `jest.useFakeTimers()` is already used in the [Jest global setup](https://github.com/mattermost/mattermost/blob/master/webapp/channels/src/tests/setup.js), but there are cases where it needs to run specifically depending on how the component uses the native timer functions.
|
||||
<br/><br/>
|
||||
|
||||
* If you get an error similar to `UnhandledPromiseRejectionWarning: TypeError: (0 , \_fff.hhh) is not a function`:
|
||||
|
||||
- Check if you're mocking part of an imported module without providing other exports which are used. You can use `jest.requireActual` to get the un-mocked version of the file.
|
||||
|
||||
```javascript
|
||||
// DO NOT partially mock the module
|
||||
jest.mock('actions/storage', () => ({
|
||||
setGlobalItem: (...args) => ({type: 'MOCK_SET_GLOBAL_ITEM', args}),
|
||||
}));
|
||||
|
||||
// DO fully mock the module
|
||||
jest.mock('actions/storage', () => {
|
||||
const original = jest.requireActual('actions/storage');
|
||||
return {
|
||||
...original,
|
||||
setGlobalItem: (...args) => ({type: 'MOCK_SET_GLOBAL_ITEM', args}),
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
* If you get an error similar to `UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'data' of undefined`:
|
||||
|
||||
- Use async mock functions with resolved values. The property in question that cannot be read can be `error`, `data`, `exists`, `match`, or whatever else the resolved value(s) contains. For example, if the file being tested contains a line where it awaits on an async value like:
|
||||
|
||||
```javascript
|
||||
const {data} = await this.props.actions.addUsersToTeam(this.props.currentTeamId, userIds);
|
||||
```
|
||||
|
||||
The mocked function should return a Promise by using `mockResolvedValue`:
|
||||
|
||||
```javascript
|
||||
// DO NOT assign a regular mock function.
|
||||
const addUsersToTeam = jest.fn();
|
||||
|
||||
// DO NOT forget to provide a resolved value.
|
||||
const addUsersToTeam: jest.fn(() => {
|
||||
return new Promise((resolve) => {
|
||||
process.nextTick(() => resolve());
|
||||
});
|
||||
}),
|
||||
|
||||
// DO mock async function with resolved value. `mockResolvedValue` is the easiest way to do this.
|
||||
const addUsersToTeam = jest.fn().mockResolvedValue({data: true})
|
||||
|
||||
// DO mock async function with several resolved values for repeated calls.
|
||||
const addUsersToTeam = jest.fn().
|
||||
mockResolvedValueOnce({error: true}).
|
||||
mockResolvedValue({data: true});
|
||||
```
|
||||
|
||||
Remember to make individual test cases async when testing async functions:
|
||||
|
||||
```javascript
|
||||
// DO NOT forget to wait for the async function to complete.
|
||||
test('should match state when handleSubmit is called', () => {
|
||||
wrapper.instance().handleSubmit();
|
||||
expect(...)
|
||||
});
|
||||
|
||||
// DO remember to wait on the async function and to make the entire test case async.
|
||||
test('should match state when handleSubmit is called', async () => {
|
||||
await wrapper.instance().handleSubmit();
|
||||
expect(addUsersToTeam).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: "Use make i18n-extract"
|
||||
sidebar_position: 10
|
||||
---
|
||||
|
||||
`make i18n-extract` is a command used for localization. It allows you to validate that your strings have been successfully extracted from your source code before you continue.
|
||||
|
||||
This page demonstrates how to review your results and to verify if your extraction was successful or not. If the extraction was not successful this page also provides a workaround to correct for this.
|
||||
|
||||
|
||||
<Note title="NOTE">
|
||||
These steps haven't been updated since Mattermost has switched to using a [monorepo](https://github.com/mattermost/mattermost). We're in the process of updating our I18n workflow and these corresponding docs.
|
||||
|
||||
In the meantime, these commands can be run from within each package (`webapp/boards`, `webapp/channels`, and `webapp/playbooks`) to update their corresponding translation files.
|
||||
</Note>
|
||||
|
||||
|
||||
1. After you execute `make i18n-extract` you will need to review the results and validate that the strings were either added or removed in the `i18n/en.json` file.
|
||||
2. Run `git diff` and determine if your strings were added or removed in the `i18n/en.json` file correctly. If this was a successful extraction you will have output similar to below:
|
||||
|
||||

|
||||
|
||||
3. However, if you have a string that was not properly extracted you will see an output similar to below. If you executed the `make i18n-extract` at this point nothing would change because the string `"new-text-id"` is not detected as a string that needs to be translated.
|
||||
|
||||

|
||||
|
||||
4. The solution is to tag the string. Do this by using the `"t"` function, shown in the example below:
|
||||
|
||||

|
||||
|
||||
5. At this point you will need to execute the `make i18n-extract` once again and determine if the extraction was successful. This will generate a message in the `i18n/en.json` file. However, this is not going to extract the "default message", you will have to add this yourself. See example below:
|
||||
|
||||

|
||||
|
||||
|
||||
<Note title="NOTE">
|
||||
Be aware that when you use the `"t"` function, only the translation id is extracted. You have to add the translation string in the `i18n/en.json` file manually.
|
||||
</Note>
|
||||
|
||||
|
||||
For further discussion about translations or to ask for help, refer to the following Mattermost channels: [Localization](https://community.mattermost.com/core/channels/localization) and [Contributors](https://community.mattermost.com/core/channels/tickets).
|
||||
Reference in New Issue
Block a user