Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions doc/adr/choose-testing-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Architectural Decision Record (ADR)

## Title: Choosing React Testing Library over Enzyme for Component Testing

### Abstract

The purpose of this document is to outline the rationale behind selecting React Testing Library (RTL) over Enzyme for
testing React components in our software development process. This decision is based on several key factors, including
testing principles, ease of use, and sustainability of testing practices.

### Decision

After a comprehensive evaluation, we have chosen to adopt React Testing Library (RTL) as our primary testing framework
for React components instead of using Enzyme.

### Rationale

#### 1. Philosophy and Principles

- RTL aligns more closely with the principles of testing within the React ecosystem. Its philosophy revolves around
testing the application from the user's perspective, focusing on behavior-driven testing. It encourages writing tests
that resemble how users interact with the application, resulting in more resilient and maintainable tests.
- Enzyme, while powerful and flexible, often promotes testing implementation details, leading to brittle tests that are
tightly coupled to the component structure. This approach can result in frequent test breakages during refactoring,
impacting development velocity.

#### 2. Simplicity and Accessibility

- RTL provides a simpler and more user-friendly API, making it easier for both experienced and novice developers to
write tests efficiently. Its straightforward querying methods using "queries" such as getByText, getByRole, etc.,
reduce the learning curve and enhance readability.
- In contrast, Enzyme's API can be complex, offering multiple ways to access and manipulate component instances, leading
to varying practices within a team and potentially hindering code comprehension and maintenance.

#### 3. Community Support and Sustainability

- The React Testing Library has gained significant traction within the React community and is endorsed by the official
React team. It enjoys active maintenance, frequent updates, and a supportive community, ensuring ongoing support and
evolution.
- Enzyme, although a popular choice in the past, has shown signs of reduced activity and official backing. The
development has slowed, and it might not keep up with evolving best practices in the React ecosystem.

#### 4. Future-Proofing and Compatibility

- As React evolves, RTL is designed to be more future-proof. It maintains compatibility with new React features and
changes in the library, reducing the likelihood of the testing framework becoming outdated.
- Enzyme's future compatibility with React's advancements might become a concern, potentially leading to the need for
migration or causing delays in adopting new React features.

### Action Plan

We will integrate RTL and all the JEST and other needed extensions in the project.

### Conclusion

The decision to adopt React Testing Library over Enzyme aligns with our commitment to robust, user-centric testing
practices, ease of use, and long-term sustainability. This transition will empower our team to write more resilient and
maintainable tests, ensuring a higher quality of our React components.

This decision is effective immediately and will be integrated into our ongoing development and testing workflows.
43 changes: 39 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "OpenStackId, IDP",
"main": "resources/assets/js/index.js",
"author": "[email protected]",
"license": "ISC",
"scripts": {
"clean": "find . -name \"node_modules\" -type d -prune -exec rm -rf '{}' + && yarn",
"build-dev": "./node_modules/.bin/webpack --config webpack.dev.js",
Expand All @@ -12,7 +13,7 @@
"test": "jest --watch"
},
"devDependencies": {
"@babel/core": "^7.17.8",
"@babel/core": "^7.23.2",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
"@babel/plugin-proposal-optional-chaining": "^7.16.7",
Expand All @@ -21,23 +22,29 @@
"@babel/preset-flow": "^7.7.4",
"@babel/preset-react": "^7.7.4",
"@babel/runtime": "^7.20.7",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "12.1.5",
"@testing-library/user-event": "^14.5.1",
"@types/jest": "^29.5.7",
"babel-cli": "^6.26.0",
"babel-jest": "^26.6.3",
"babel-jest": "^29.7.0",
"babel-loader": "^8.2.4",
"babel-plugin-react-remove-properties": "^0.3.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^6.4.1",
"core-js": "^3.33.2",
"css-loader": "^6.7.1",
"dotenv-webpack": "^1.7.0",
"file-loader": "^6.2.0",
"font-awesome": "4.7.0",
"history": "^4.7.2",
"html-webpack-plugin": "^3.2.0",
"i18n-react": "^0.6.4",
"identity-obj-proxy": "^3.0.0",
"immutability-helper": "^2.7.1",
"jest": "^26.6.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-transform-stub": "^2.0.0",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"mini-css-extract-plugin": "^2.6.0",
Expand Down Expand Up @@ -110,5 +117,33 @@
"typeahead.js": "^0.11.1",
"urijs": "^1.19.1",
"yup": "^0.32.11"
},
"jest": {
"collectCoverageFrom": [
"resources/js/**/*.{js,jsx,mjs}"
],
"moduleDirectories": [
"node_modules",
"resources/js"
],
"moduleFileExtensions": [
"js",
"json",
"jsx",
"node",
"mjs"
],
"moduleNameMapper": {
"\\.(css|scss)$": "identity-obj-proxy"
},
"testEnvironment": "jsdom",
"testMatch": [
"<rootDir>/resources/js/**/__tests__/**/*.{js,jsx,mjs}",
"<rootDir>/resources/js/**/?(*.)(spec|test).{js,jsx,mjs}"
],
"transform": {
"\\.[jt]sx?$": "babel-jest",
".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub"
}
}
}
96 changes: 96 additions & 0 deletions resources/js/admin/edit_user/__test__/edit_user.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from 'react'
import {fireEvent, render, screen, waitFor} from '@testing-library/react';
import '@testing-library/jest-dom'
import {EditUserPage} from '../edit_user'

const props = {
countries: [{value: "CA", text: "Canada"}],
csrfToken: '',
initialValues: {
id: 1,
pic: '',
language: 'en',
country_iso_code: 'CA',
gender: 'Male',
first_name: 'Test',
last_name: 'User',
email: '[email protected]'
},
languages: [
{value: "en", text: "English"},
{value: "cs", text: "Czech"}
],
passwordPolicy: {
min_length: 8,
max_length: 30,
},
menuConfig: {
settingsText: ''
}
}

const dummyUserActions = {
"total": 1,
"per_page": 10,
"current_page": 1,
"last_page": 1,
"data": [
{
"id": 10428,
"created_at": 1607162809,
"updated_at": 1607162809,
"realm": "https:\/\/infinityfestival2020.fnvirtual.app\/auth\/callback?BackUrl=%252Fa%252F",
"user_action": "LOGIN",
"from_ip": "80.5.135.101"
}
]
}


jest.mock('../actions', () => {
return {
__esModule: true,
getUserActions: jest.fn(() => Promise.resolve(dummyUserActions)),
save: jest.fn(() => Promise.resolve()),
PAGE_SIZE: 50
};
});

const mockChildComponent = jest.fn();
jest.mock('../../../components/user_actions_grid', () => (props) => {
mockChildComponent(props);
return <div/>;
})

afterEach(() => {
jest.clearAllMocks();
});

describe('EditUserPage', () => {
test('form is populated', () => {
render(<EditUserPage {...props} />)
expect(screen.getByRole('textbox', {name: /first name/i})).toHaveValue()
expect(screen.getByRole('textbox', {name: /last name/i})).toHaveValue()
expect(screen.getByRole('textbox', {name: 'Email'})).toHaveValue()
})

test("user actions grid component is called once", () => {
render(<EditUserPage {...props} />)
expect(mockChildComponent).toHaveBeenCalledTimes(1);
});

test('submitting the form with empty fields shows validation error', async () => {
const {getByRole, getByText} = render(<EditUserPage {...props} />)

const input = getByRole('textbox', {name: /first name/i})
fireEvent.change(input, {target: {value: ''}});

const submitButton = getByText('Save');
fireEvent.click(submitButton);

await waitFor(() => {
expect(getByText('First name is required')).toBeInTheDocument();
});
});
})

64 changes: 12 additions & 52 deletions resources/js/admin/edit_user/edit_user.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, {useEffect, useState} from "react";
import ReactDOM from "react-dom";
import React, {useState} from "react";
import ArrowBack from '@material-ui/icons/ArrowBack';
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
Expand All @@ -14,7 +13,6 @@ import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import Select from "@material-ui/core/Select";
import Swal from "sweetalert2";
import {MuiThemeProvider, createTheme} from "@material-ui/core/styles";
import {useFormik} from "formik";
import {object, ref, string} from "yup";
import RichTextEditor from "../../components/rich_text_editor";
Expand All @@ -32,17 +30,17 @@ import TopLogo from "../../components/top_logo/top_logo";
import styles from "./edit_user.module.scss";
import {handleErrorResponse} from "../../utils";

const EditUserPage = ({
appLogo,
countries,
csrfToken,
fetchGroupsURL,
initialValues,
languages,
menuConfig,
passwordPolicy,
usersListURL
}) => {
export const EditUserPage = ({
appLogo,
countries,
csrfToken,
fetchGroupsURL,
initialValues,
languages,
menuConfig,
passwordPolicy,
usersListURL
}) => {
const [pic, setPic] = useState(null);

// intialize current groups
Expand Down Expand Up @@ -765,41 +763,3 @@ const EditUserPage = ({
);
};

// Or Create your Own theme:
const theme = createTheme({
palette: {
primary: {
main: "#3fa2f7",
},
},
overrides: {
MuiButton: {
containedPrimary: {
color: "white",
},
},
},
});

Object.assign(theme, {
overrides: {
MUIRichTextEditor: {
root: {
marginTop: 5,
height: 400,
border: "1px solid #D3D3D3",
borderRadius: "5px"
},
editor: {
borderTop: "1px solid #D3D3D3"
}
}
}
})

ReactDOM.render(
<MuiThemeProvider theme={theme}>
<EditUserPage {...config} />
</MuiThemeProvider>,
document.querySelector("#root")
);
42 changes: 42 additions & 0 deletions resources/js/admin/edit_user/edit_user_wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from "react";
import ReactDOM from "react-dom";
import {EditUserPage} from "./edit_user";
import {MuiThemeProvider, createTheme} from "@material-ui/core/styles";

const theme = createTheme({
palette: {
primary: {
main: "#3fa2f7",
},
},
overrides: {
MuiButton: {
containedPrimary: {
color: "white",
},
},
},
});

Object.assign(theme, {
overrides: {
MUIRichTextEditor: {
root: {
marginTop: 5,
height: 400,
border: "1px solid #D3D3D3",
borderRadius: "5px"
},
editor: {
borderTop: "1px solid #D3D3D3"
}
}
}
})

ReactDOM.render(
<MuiThemeProvider theme={theme}>
<EditUserPage {...config} />
</MuiThemeProvider>,
document.querySelector("#root")
);
Loading