diff --git a/backend/graphql/schema.graphql b/backend/graphql/schema.graphql index c59d125a5..2a47dafcf 100644 --- a/backend/graphql/schema.graphql +++ b/backend/graphql/schema.graphql @@ -3,14 +3,8 @@ type Query { } type Mutation { - createBankAccount( - bankName: String!, - accountNumber: String!, - routingNumber: String! - ): BankAccount - deleteBankAccount( - id: ID! - ): Boolean + createBankAccount(bankName: String!, accountNumber: String!, routingNumber: String!): BankAccount + deleteBankAccount(id: ID!): Boolean } type BankAccount { @@ -23,4 +17,4 @@ type BankAccount { isDeleted: Boolean createdAt: String modifiedAt: String -} \ No newline at end of file +} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 64e63b6ab..c206e87f8 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -123,6 +123,7 @@ Cypress.Commands.add("setTransactionAmountRange", (min, max) => { .getBySelLike("filter-amount-range-slider") .reactComponent() .its("memoizedProps") + .its("ownerState") .invoke("onChange", null, [min / 10, max / 10]); }); diff --git a/cypress/support/component-index.html b/cypress/support/component-index.html index 1af394f61..faf3b5f43 100644 --- a/cypress/support/component-index.html +++ b/cypress/support/component-index.html @@ -1,14 +1,12 @@ - + - - - + + + Components App - -
diff --git a/cypress/tests/api/api-contacts.spec.ts b/cypress/tests/api/api-contacts.spec.ts index 2969b3949..d21d1b1dd 100644 --- a/cypress/tests/api/api-contacts.spec.ts +++ b/cypress/tests/api/api-contacts.spec.ts @@ -53,7 +53,7 @@ describe("Contacts API", function () { }); }); - it("error when invalid contactUserId", function () { + it("errors when invalid contactUserId", function () { cy.request({ method: "POST", url: `${apiContacts}`, diff --git a/cypress/tests/api/api-notifications.spec.ts b/cypress/tests/api/api-notifications.spec.ts index 97650de64..bf51866ae 100644 --- a/cypress/tests/api/api-notifications.spec.ts +++ b/cypress/tests/api/api-notifications.spec.ts @@ -90,7 +90,7 @@ describe("Notifications API", function () { }); }); - it("error when invalid field sent", function () { + it("errors when invalid field sent", function () { cy.request({ method: "PATCH", url: `${apiNotifications}/${ctx.notificationId}`, diff --git a/cypress/tests/api/api-transactions.spec.ts b/cypress/tests/api/api-transactions.spec.ts index 937c29622..1ae895080 100644 --- a/cypress/tests/api/api-transactions.spec.ts +++ b/cypress/tests/api/api-transactions.spec.ts @@ -152,7 +152,7 @@ describe("Transactions API", function () { }); }); - it("error when invalid field sent", function () { + it("errors when an invalid field sent", function () { cy.request({ method: "PATCH", url: `${apiTransactions}/${ctx.transactionId}`, diff --git a/cypress/tests/api/api-users.spec.ts b/cypress/tests/api/api-users.spec.ts index caa26df07..2e0daebe3 100644 --- a/cypress/tests/api/api-users.spec.ts +++ b/cypress/tests/api/api-users.spec.ts @@ -37,14 +37,14 @@ describe("Users API", function () { }); context("GET /users/:userId", function () { - it("get a user", function () { + it("gets a user", function () { cy.request("GET", `${apiUsers}/${ctx.authenticatedUser!.id}`).then((response) => { expect(response.status).to.eq(200); expect(response.body.user).to.have.property("firstName"); }); }); - it("error when invalid userId", function () { + it("errors when invalid userId", function () { cy.request({ method: "GET", url: `${apiUsers}/1234`, @@ -57,7 +57,7 @@ describe("Users API", function () { }); context("GET /users/profile/:username", function () { - it("get a user profile by username", function () { + it("gets a user profile by username", function () { const { username, firstName, lastName, avatar } = ctx.authenticatedUser!; cy.request("GET", `${apiUsers}/profile/${username}`).then((response) => { expect(response.status).to.eq(200); @@ -72,7 +72,7 @@ describe("Users API", function () { }); context("GET /users/search", function () { - it("get users by email", function () { + it("gets users by email", function () { const { email, firstName } = ctx.searchUser!; cy.request({ method: "GET", @@ -86,7 +86,7 @@ describe("Users API", function () { }); }); - it("get users by phone number", function () { + it("gets users by phone number", function () { const { phoneNumber, firstName } = ctx.searchUser!; cy.request({ @@ -101,7 +101,7 @@ describe("Users API", function () { }); }); - it("get users by username", function () { + it("gets users by username", function () { const { username, firstName } = ctx.searchUser!; cy.request({ @@ -154,7 +154,7 @@ describe("Users API", function () { }); }); - it("error when invalid field sent", function () { + it("errors when an invalid field sent", function () { cy.request({ method: "POST", url: `${apiUsers}`, @@ -180,7 +180,7 @@ describe("Users API", function () { }); }); - it("error when invalid field sent", function () { + it("errors when an invalid field sent", function () { cy.request({ method: "PATCH", url: `${apiUsers}/${ctx.authenticatedUser!.id}`, @@ -196,7 +196,7 @@ describe("Users API", function () { }); context("POST /login", function () { - it("login as user", function () { + it("logs in as a user", function () { cy.loginByApi(ctx.authenticatedUser!.username).then((response) => { expect(response.status).to.eq(200); }); diff --git a/cypress/tests/ui/notifications.spec.ts b/cypress/tests/ui/notifications.spec.ts index 4007d7406..12c2faec2 100644 --- a/cypress/tests/ui/notifications.spec.ts +++ b/cypress/tests/ui/notifications.spec.ts @@ -26,65 +26,59 @@ describe("Notifications", function () { }); describe("notifications from user interactions", function () { - it( - "User A likes a transaction of User B; User B gets notification that User A liked transaction ", - // NOTE: this test seems to have issues in Firefox UI/Mobile tests due to an issue with the Base Button component in MUI v4 - // we should try unskipping this test in Firefox once we upgrade MUI to v5+. @see https://github.com/cypress-io/cypress-realworld-app/issues/1278 - { browser: { name: "!firefox" } }, - function () { - cy.loginByXstate(ctx.userA.username); - cy.wait("@getNotifications"); - - cy.database("find", "transactions", { senderId: ctx.userB.id }).then( - (transaction: Transaction) => { - cy.visit(`/transaction/${transaction.id}`); - } - ); - - cy.log("🚩 Renders the notifications badge with count"); - cy.wait("@getNotifications") - .its("response.body.results.length") - .then((notificationCount) => { - cy.getBySel("nav-top-notifications-count").should("have.text", `${notificationCount}`); - }); - cy.visualSnapshot("Renders the notifications badge with count"); - - const likesCountSelector = "[data-test*=transaction-like-count]"; - cy.contains(likesCountSelector, 0); - cy.getBySelLike("like-button").click(); - // a successful "like" should disable the button and increment - // the number of likes - cy.getBySelLike("like-button").should("be.disabled"); - cy.contains(likesCountSelector, 1); - cy.visualSnapshot("Like Count Incremented"); - - cy.switchUserByXstate(ctx.userB.username); - cy.visualSnapshot(`Switch to User ${ctx.userB.username}`); - - cy.wait("@getNotifications") - .its("response.body.results.length") - .as("preDismissedNotificationCount"); - - cy.visit("/notifications"); - - cy.wait("@getNotifications"); - - cy.getBySelLike("notification-list-item") - .should("have.length", 9) - .first() - .should("contain", ctx.userA?.firstName) - .and("contain", "liked"); - - cy.log("🚩 Marks notification as read"); - cy.getBySelLike("notification-mark-read").first().click({ force: true }); - cy.wait("@updateNotification"); - - cy.get("@preDismissedNotificationCount").then((count) => { - cy.getBySelLike("notification-list-item").should("have.length.lessThan", Number(count)); + it("User A likes a transaction of User B; User B gets notification that User A liked transaction ", function () { + cy.loginByXstate(ctx.userA.username); + cy.wait("@getNotifications"); + + cy.database("find", "transactions", { senderId: ctx.userB.id }).then( + (transaction: Transaction) => { + cy.visit(`/transaction/${transaction.id}`); + } + ); + + cy.log("🚩 Renders the notifications badge with count"); + cy.wait("@getNotifications") + .its("response.body.results.length") + .then((notificationCount) => { + cy.getBySel("nav-top-notifications-count").should("have.text", `${notificationCount}`); }); - cy.visualSnapshot("Notification count after notification dismissed"); - } - ); + cy.visualSnapshot("Renders the notifications badge with count"); + + const likesCountSelector = "[data-test*=transaction-like-count]"; + cy.contains(likesCountSelector, 0); + cy.getBySelLike("like-button").click(); + // a successful "like" should disable the button and increment + // the number of likes + cy.getBySelLike("like-button").should("be.disabled"); + cy.contains(likesCountSelector, 1); + cy.visualSnapshot("Like Count Incremented"); + + cy.switchUserByXstate(ctx.userB.username); + cy.visualSnapshot(`Switch to User ${ctx.userB.username}`); + + cy.wait("@getNotifications") + .its("response.body.results.length") + .as("preDismissedNotificationCount"); + + cy.visit("/notifications"); + + cy.wait("@getNotifications"); + + cy.getBySelLike("notification-list-item") + .should("have.length", 9) + .first() + .should("contain", ctx.userA?.firstName) + .and("contain", "liked"); + + cy.log("🚩 Marks notification as read"); + cy.getBySelLike("notification-mark-read").first().click({ force: true }); + cy.wait("@updateNotification"); + + cy.get("@preDismissedNotificationCount").then((count) => { + cy.getBySelLike("notification-list-item").should("have.length.lessThan", Number(count)); + }); + cy.visualSnapshot("Notification count after notification dismissed"); + }); it("User C likes a transaction between User A and User B; User A and User B get notifications that User C liked transaction", function () { cy.loginByXstate(ctx.userC.username); diff --git a/cypress/tests/ui/transaction-view.spec.ts b/cypress/tests/ui/transaction-view.spec.ts index c18b767ae..dd4ae54e4 100644 --- a/cypress/tests/ui/transaction-view.spec.ts +++ b/cypress/tests/ui/transaction-view.spec.ts @@ -40,7 +40,8 @@ describe("Transaction View", function () { }); it("transactions navigation tabs are hidden on a transaction view page", function () { - cy.getBySelLike("transaction-item").first().click(); + // { force: true } is a workaround for https://github.com/cypress-io/cypress/issues/29776 + cy.getBySelLike("transaction-item").first().click({ force: true }); cy.location("pathname").should("include", "/transaction"); cy.getBySel("nav-transaction-tabs").should("not.exist"); cy.getBySel("transaction-detail-header").should("be.visible"); @@ -48,7 +49,8 @@ describe("Transaction View", function () { }); it("likes a transaction", function () { - cy.getBySelLike("transaction-item").first().click(); + // { force: true } is a workaround for https://github.com/cypress-io/cypress/issues/29776 + cy.getBySelLike("transaction-item").first().click({ force: true }); cy.wait("@getTransaction"); cy.getBySelLike("like-button").click(); @@ -58,7 +60,8 @@ describe("Transaction View", function () { }); it("comments on a transaction", function () { - cy.getBySelLike("transaction-item").first().click(); + // { force: true } is a workaround for https://github.com/cypress-io/cypress/issues/29776 + cy.getBySelLike("transaction-item").first().click({ force: true }); cy.wait("@getTransaction"); const comments = ["Thank you!", "Appreciate it."]; diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 2810324da..e32dac45f 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -7,6 +7,6 @@ "lib": ["es2015", "dom"], "isolatedModules": false, "allowJs": true, - "noEmit": true - } + "noEmit": true, + }, } diff --git a/package.json b/package.json index b60c54004..1a8d1dd8d 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,14 @@ "@babel/core": "7.23.9", "@babel/plugin-syntax-flow": "^7.14.5", "@babel/plugin-transform-react-jsx": "^7.14.9", + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.0", "@graphql-tools/graphql-file-loader": "7.5.17", "@graphql-tools/load": "7.8.14", - "@material-ui/core": "4.12.4", - "@material-ui/icons": "4.11.3", - "@material-ui/lab": "4.0.0-alpha.61", "@matheusluizn/react-google-login": "^5.1.6", + "@mui/icons-material": "^5.15.12", + "@mui/lab": "^5.0.0-alpha.167", + "@mui/material": "^5.15.12", "@okta/jwt-verifier": "^3.0.1", "@okta/okta-auth-js": "^7.3.0", "@okta/okta-react": "^6.7.0", @@ -100,9 +102,9 @@ "eslint": "^8.44.0", "eslint-config-prettier": "8.10.0", "eslint-config-react-app": "^7.0.1", - "eslint-plugin-cypress": "2.15.2", + "eslint-plugin-cypress": "3.5.0", "eslint-plugin-prettier": "^5.0.0", - "express": "4.19.2", + "express": "4.20.0", "express-jwt": "6.1.2", "express-paginate": "1.0.2", "express-session": "1.18.0", diff --git a/patches/@material-ui+core+4.12.4.patch b/patches/@material-ui+core+4.12.4.patch deleted file mode 100644 index 696ff8764..000000000 --- a/patches/@material-ui+core+4.12.4.patch +++ /dev/null @@ -1,103 +0,0 @@ -diff --git a/node_modules/@material-ui/core/ButtonBase/ButtonBase.js b/node_modules/@material-ui/core/ButtonBase/ButtonBase.js -index 4972129..6516a00 100644 ---- a/node_modules/@material-ui/core/ButtonBase/ButtonBase.js -+++ b/node_modules/@material-ui/core/ButtonBase/ButtonBase.js -@@ -158,11 +158,23 @@ var ButtonBase = /*#__PURE__*/React.forwardRef(function ButtonBase(props, ref) { - } - }; - }, []); -+ -+ var _React$useState2 = React.useState(false), -+ mountedState = _React$useState2[0], -+ setMountedState = _React$useState2[1]; -+ -+ React.useEffect(function () { -+ setMountedState(true); -+ }, []); -+ var enableTouchRipple = mountedState && !disableRipple && !disabled; -+ - React.useEffect(function () { -- if (focusVisible && focusRipple && !disableRipple) { -- rippleRef.current.pulsate(); -+ if (focusVisible && focusRipple && !disableRipple && mountedState) { -+ if (rippleRef.current) { -+ rippleRef.current.pulsate(); -+ } - } -- }, [disableRipple, focusRipple, focusVisible]); -+ }, [disableRipple, focusRipple, focusVisible, mountedState]); - - function useRippleHandler(rippleAction, eventCallback) { - var skipRippleAction = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : disableTouchRipple; -@@ -269,7 +281,9 @@ var ButtonBase = /*#__PURE__*/React.forwardRef(function ButtonBase(props, ref) { - keydownRef.current = false; - event.persist(); - rippleRef.current.stop(event, function () { -- rippleRef.current.pulsate(event); -+ if (rippleRef.current) { -+ rippleRef.current.pulsate(event); -+ } - }); - } - -@@ -305,15 +319,6 @@ var ButtonBase = /*#__PURE__*/React.forwardRef(function ButtonBase(props, ref) { - var handleOwnRef = (0, _useForkRef.default)(focusVisibleRef, buttonRef); - var handleRef = (0, _useForkRef.default)(handleUserRef, handleOwnRef); - -- var _React$useState2 = React.useState(false), -- mountedState = _React$useState2[0], -- setMountedState = _React$useState2[1]; -- -- React.useEffect(function () { -- setMountedState(true); -- }, []); -- var enableTouchRipple = mountedState && !disableRipple && !disabled; -- - if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line react-hooks/rules-of-hooks - React.useEffect(function () { -diff --git a/node_modules/@material-ui/core/es/ButtonBase/ButtonBase.js b/node_modules/@material-ui/core/es/ButtonBase/ButtonBase.js -index b4be26f..06b0d63 100644 ---- a/node_modules/@material-ui/core/es/ButtonBase/ButtonBase.js -+++ b/node_modules/@material-ui/core/es/ButtonBase/ButtonBase.js -@@ -125,7 +125,9 @@ const ButtonBase = /*#__PURE__*/React.forwardRef(function ButtonBase(props, ref) - }), []); - React.useEffect(() => { - if (focusVisible && focusRipple && !disableRipple) { -- rippleRef.current.pulsate(); -+ if (rippleRef.current) { -+ rippleRef.current.pulsate(); -+ } - } - }, [disableRipple, focusRipple, focusVisible]); - -diff --git a/node_modules/@material-ui/core/esm/ButtonBase/ButtonBase.js b/node_modules/@material-ui/core/esm/ButtonBase/ButtonBase.js -index 58cfc64..de2db57 100644 ---- a/node_modules/@material-ui/core/esm/ButtonBase/ButtonBase.js -+++ b/node_modules/@material-ui/core/esm/ButtonBase/ButtonBase.js -@@ -136,7 +136,9 @@ var ButtonBase = /*#__PURE__*/React.forwardRef(function ButtonBase(props, ref) { - }, []); - React.useEffect(function () { - if (focusVisible && focusRipple && !disableRipple) { -- rippleRef.current.pulsate(); -+ if (rippleRef.current) { -+ rippleRef.current.pulsate(); -+ } - } - }, [disableRipple, focusRipple, focusVisible]); - -diff --git a/node_modules/@material-ui/core/umd/material-ui.development.js b/node_modules/@material-ui/core/umd/material-ui.development.js -index ad23fd3..fb4cb53 100644 ---- a/node_modules/@material-ui/core/umd/material-ui.development.js -+++ b/node_modules/@material-ui/core/umd/material-ui.development.js -@@ -11208,7 +11208,9 @@ - }, []); - React.useEffect(function () { - if (focusVisible && focusRipple && !disableRipple) { -- rippleRef.current.pulsate(); -+ if (rippleRef.current) { -+ rippleRef.current.pulsate(); -+ } - } - }, [disableRipple, focusRipple, focusVisible]); - diff --git a/renovate.json b/renovate.json index ec472d9b1..7bac9ce56 100644 --- a/renovate.json +++ b/renovate.json @@ -68,7 +68,7 @@ }, { "groupName": "Material UI", - "matchPackagePatterns": ["^@material-ui/"] + "matchPackagePatterns": ["^@mui/"] }, { "groupName": "Graphql", diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json index 555eec32d..8ad4852db 100644 --- a/scripts/tsconfig.json +++ b/scripts/tsconfig.json @@ -4,6 +4,6 @@ "exclude": [], "compilerOptions": { "types": ["node"], - "isolatedModules": false - } + "isolatedModules": false, + }, } diff --git a/src/components/AlertBar.tsx b/src/components/AlertBar.tsx index 41fb79282..293d4703e 100644 --- a/src/components/AlertBar.tsx +++ b/src/components/AlertBar.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Snackbar } from "@material-ui/core"; +import { Snackbar } from "@mui/material"; import { BaseActionObject, Interpreter, @@ -9,7 +9,7 @@ import { } from "xstate"; import { SnackbarContext, SnackbarSchema, SnackbarEvents } from "../machines/snackbarMachine"; import { useActor } from "@xstate/react"; -import { Alert } from "@material-ui/lab"; +import { Alert } from "@mui/material"; interface Props { snackbarService: Interpreter< diff --git a/src/components/BankAccountForm.tsx b/src/components/BankAccountForm.tsx index d360362dc..aa67eb6be 100644 --- a/src/components/BankAccountForm.tsx +++ b/src/components/BankAccountForm.tsx @@ -1,5 +1,6 @@ import React from "react"; -import { makeStyles, TextField, Button, Grid } from "@material-ui/core"; +import { styled } from "@mui/material/styles"; +import { TextField, Button, Grid } from "@mui/material"; import { Formik, Form, Field, FieldProps } from "formik"; import { string, object } from "yup"; import { BankAccountPayload, User } from "../models"; @@ -16,18 +17,28 @@ const validationSchema = object({ .required("Enter a valid bank account number"), }); -const useStyles = makeStyles((theme) => ({ - paper: { +const PREFIX = "BankAccountForm"; + +const classes = { + paper: `${PREFIX}-paper`, + form: `${PREFIX}-form`, + submit: `${PREFIX}-submit`, +}; + +const StyledFormik = styled(Formik)(({ theme }) => ({ + [`& .${classes.paper}`]: { marginTop: theme.spacing(8), display: "flex", flexDirection: "column", alignItems: "center", }, - form: { + + [`& .${classes.form}`]: { width: "100%", // Fix IE 11 issue. marginTop: theme.spacing(1), }, - submit: { + + [`& .${classes.submit}`]: { margin: theme.spacing(3, 0, 2), }, })); @@ -44,7 +55,7 @@ const BankAccountForm: React.FC = ({ onboarding, }) => { const history = useHistory(); - const classes = useStyles(); + const initialValues: BankAccountPayload = { userId, bankName: "", @@ -53,7 +64,7 @@ const BankAccountForm: React.FC = ({ }; return ( - { @@ -119,7 +130,13 @@ const BankAccountForm: React.FC = ({ /> )} - + )} - + ); }; diff --git a/src/components/SignInForm.tsx b/src/components/SignInForm.tsx index e2c641a43..95a99f4e2 100644 --- a/src/components/SignInForm.tsx +++ b/src/components/SignInForm.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { styled } from "@mui/material/styles"; import { Interpreter } from "xstate"; import { useActor } from "@xstate/react"; import { Link } from "react-router-dom"; @@ -11,9 +12,8 @@ import { Grid, Box, Typography, - makeStyles, Container, -} from "@material-ui/core"; +} from "@mui/material"; import { Formik, Form, Field, FieldProps } from "formik"; import { string, object } from "yup"; @@ -21,7 +21,7 @@ import RWALogo from "./SvgRwaLogo"; import Footer from "./Footer"; import { SignInPayload } from "../models"; import { AuthMachineContext, AuthMachineEvents, AuthMachineSchema } from "../machines/authMachine"; -import { Alert } from "@material-ui/lab"; +import { Alert } from "@mui/material"; const validationSchema = object({ username: string().required("Username is required"), @@ -30,34 +30,47 @@ const validationSchema = object({ .required("Enter your password"), }); -const useStyles = makeStyles((theme) => ({ - paper: { +const PREFIX = "SignInForm"; + +const classes = { + paper: `${PREFIX}-paper`, + logo: `${PREFIX}-logo`, + form: `${PREFIX}-form`, + submit: `${PREFIX}-submit`, + alertMessage: `${PREFIX}-alertMessage`, +}; + +const StyledContainer = styled(Container)(({ theme }) => ({ + [`& .${classes.paper}`]: { marginTop: theme.spacing(8), display: "flex", flexDirection: "column", alignItems: "center", }, - logo: { + + [`& .${classes.logo}`]: { color: theme.palette.primary.main, }, - form: { + + [`& .${classes.form}`]: { width: "100%", // Fix IE 11 issue. marginTop: theme.spacing(1), }, - submit: { + + [`& .${classes.submit}`]: { margin: theme.spacing(3, 0, 2), }, - alertMessage: { + + [`& .${classes.alertMessage}`]: { marginBottom: theme.spacing(2), }, -})); +})) as typeof Container; export interface Props { authService: Interpreter; } const SignInForm: React.FC = ({ authService }) => { - const classes = useStyles(); const [authState, sendAuth] = useActor(authService); const initialValues: SignInPayload = { username: "", @@ -68,7 +81,7 @@ const SignInForm: React.FC = ({ authService }) => { const signInPending = (payload: SignInPayload) => sendAuth({ type: "LOGIN", ...payload }); return ( - +
{authState.context?.message && ( @@ -164,7 +177,7 @@ const SignInForm: React.FC = ({ authService }) => {