diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..301fce0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 10 + + - package-ecosystem: docker + directory: / + schedule: + interval: weekly + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fef7f7e..0d77f9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,10 +121,10 @@ jobs: run: docker compose -f compose.yml -f compose.dev.yml up -d mongo redis - name: Start server - run: LETSCUBE_TEST_AUTH=true SOCIAL_FEATURES_ENABLED=true yarn workspace letscube-server node index.js > server.log 2>&1 & + run: LETSCUBE_TEST_AUTH=true SOCIAL_FEATURES_ENABLED=true POSTGRES_ENABLED=false yarn workspace letscube-server node index.js > server.log 2>&1 & - name: Start socket server - run: SOCIAL_FEATURES_ENABLED=true yarn workspace letscube-server node socket/ > socket.log 2>&1 & + run: SOCIAL_FEATURES_ENABLED=true POSTGRES_ENABLED=false yarn workspace letscube-server node socket/ > socket.log 2>&1 & - name: Wait for local stack run: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..700b698 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,33 @@ +name: CodeQL + +on: + pull_request: + push: + branches: + - master + schedule: + - cron: '23 10 * * 1' + +permissions: + contents: read + +jobs: + analyze: + name: Analyze JavaScript + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: javascript-typescript + + - name: Analyze + uses: github/codeql-action/analyze@v4 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..4f3315a --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +yarn lint && yarn test diff --git a/client/.eslintignore b/client/.eslintignore deleted file mode 100644 index 12a8fb3..0000000 --- a/client/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -build/** -config/** -scripts/** \ No newline at end of file diff --git a/client/.eslintrc b/client/.eslintrc deleted file mode 100644 index 2ae4b20..0000000 --- a/client/.eslintrc +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": [ - "react-app", - "airbnb", - "plugin:jsx-a11y/recommended" - ], - "plugins": [ - "jsx-a11y" - ], - "rules": { - "semi": 1, - "no-underscore-dangle": 0, - "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], - "no-console": "warn", - "react/jsx-props-no-spreading": [1, { - "custom": "ignore" - }] - } -} \ No newline at end of file diff --git a/client/babel.config.cjs b/client/babel.config.cjs new file mode 100644 index 0000000..9349078 --- /dev/null +++ b/client/babel.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + presets: [ + ['@babel/preset-env', { targets: { node: 'current' } }], + ['@babel/preset-react', { runtime: 'automatic' }], + ], +}; diff --git a/client/config/env.js b/client/config/env.js deleted file mode 100644 index 856fc7d..0000000 --- a/client/config/env.js +++ /dev/null @@ -1,93 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const paths = require('./paths'); - -// Make sure that including paths.js after env.js will read .env variables. -delete require.cache[require.resolve('./paths')]; - -const NODE_ENV = process.env.NODE_ENV; -if (!NODE_ENV) { - throw new Error( - 'The NODE_ENV environment variable is required but was not specified.' - ); -} - -// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use -const dotenvFiles = [ - `${paths.dotenv}.${NODE_ENV}.local`, - `${paths.dotenv}.${NODE_ENV}`, - // Don't include `.env.local` for `test` environment - // since normally you expect tests to produce the same - // results for everyone - NODE_ENV !== 'test' && `${paths.dotenv}.local`, - paths.dotenv, -].filter(Boolean); - -// Load environment variables from .env* files. Suppress warnings using silent -// if this file is missing. dotenv will never modify any environment variables -// that have already been set. Variable expansion is supported in .env files. -// https://github.com/motdotla/dotenv -// https://github.com/motdotla/dotenv-expand -dotenvFiles.forEach(dotenvFile => { - if (fs.existsSync(dotenvFile)) { - require('dotenv-expand')( - require('dotenv').config({ - path: dotenvFile, - }) - ); - } -}); - -// We support resolving modules according to `NODE_PATH`. -// This lets you use absolute paths in imports inside large monorepos: -// https://github.com/facebook/create-react-app/issues/253. -// It works similar to `NODE_PATH` in Node itself: -// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders -// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. -// Otherwise, we risk importing Node.js core modules into the browser bundle. -// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 -// We also resolve them to make sure all tools using them work consistently. -const appDirectory = fs.realpathSync(process.cwd()); -process.env.NODE_PATH = (process.env.NODE_PATH || '') - .split(path.delimiter) - .filter(folder => folder && !path.isAbsolute(folder)) - .map(folder => path.resolve(appDirectory, folder)) - .join(path.delimiter); - -// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be -// injected into the application by the frontend build configuration. -const REACT_APP = /^REACT_APP_/i; - -function getClientEnvironment(publicUrl) { - const raw = Object.keys(process.env) - .filter(key => REACT_APP.test(key)) - .reduce( - (env, key) => { - env[key] = process.env[key]; - return env; - }, - { - // Useful for determining whether we’re running in production mode. - // Most importantly, it switches React into the correct mode. - NODE_ENV: process.env.NODE_ENV || 'development', - // Useful for resolving the correct path to static assets in `public`. - // For example, . - // This should only be used as an escape hatch. Normally you would put - // images into the `src` and `import` them in code to get their paths. - PUBLIC_URL: publicUrl, - } - ); - // Stringify all values for Jest's Babel-based test environment. - const stringified = { - 'process.env': Object.keys(raw).reduce((env, key) => { - env[key] = JSON.stringify(raw[key]); - return env; - }, {}), - }; - - return { raw, stringified }; -} - -module.exports = getClientEnvironment; diff --git a/client/config/jest/cssTransform.js b/client/config/jest/cssTransform.js deleted file mode 100644 index 8f65114..0000000 --- a/client/config/jest/cssTransform.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -// This is a custom Jest transformer turning style imports into empty objects. -// http://facebook.github.io/jest/docs/en/webpack.html - -module.exports = { - process() { - return 'module.exports = {};'; - }, - getCacheKey() { - // The output is always the same. - return 'cssTransform'; - }, -}; diff --git a/client/config/jest/fileTransform.js b/client/config/jest/fileTransform.js deleted file mode 100644 index aab6761..0000000 --- a/client/config/jest/fileTransform.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -const path = require('path'); -const camelcase = require('camelcase'); - -// This is a custom Jest transformer turning file imports into filenames. -// http://facebook.github.io/jest/docs/en/webpack.html - -module.exports = { - process(src, filename) { - const assetFilename = JSON.stringify(path.basename(filename)); - - if (filename.match(/\.svg$/)) { - // Based on how SVGR generates a component name: - // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 - const pascalCaseFilename = camelcase(path.parse(filename).name, { - pascalCase: true, - }); - const componentName = `Svg${pascalCaseFilename}`; - return `const React = require('react'); - module.exports = { - __esModule: true, - default: ${assetFilename}, - ReactComponent: React.forwardRef(function ${componentName}(props, ref) { - return { - $$typeof: Symbol.for('react.element'), - type: 'svg', - ref: ref, - key: null, - props: Object.assign({}, props, { - children: ${assetFilename} - }) - }; - }), - };`; - } - - return `module.exports = ${assetFilename};`; - }, -}; diff --git a/client/config/paths.js b/client/config/paths.js deleted file mode 100644 index a53d246..0000000 --- a/client/config/paths.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; - -const path = require('path'); -const fs = require('fs'); -const url = require('url'); - -// Make sure any symlinks in the project folder are resolved: -// https://github.com/facebook/create-react-app/issues/637 -const appDirectory = fs.realpathSync(process.cwd()); -const resolveApp = relativePath => path.resolve(appDirectory, relativePath); - -const envPublicUrl = process.env.PUBLIC_URL; - -function ensureSlash(inputPath, needsSlash) { - const hasSlash = inputPath.endsWith('/'); - if (hasSlash && !needsSlash) { - return inputPath.substr(0, inputPath.length - 1); - } else if (!hasSlash && needsSlash) { - return `${inputPath}/`; - } else { - return inputPath; - } -} - -const getPublicUrl = appPackageJson => - envPublicUrl || require(appPackageJson).homepage; - -// We use `PUBLIC_URL` environment variable or "homepage" field to infer -// "public path" at which the app is served. -// The build needs to know it to put the right asset hrefs into HTML even in -// single-page apps that may serve index.html for nested URLs like /todos/42. -// We can't use a relative path in HTML because we don't want to load something -// like /todos/42/static/js/bundle.7289d.js. We have to know the root. -function getServedPath(appPackageJson) { - const publicUrl = getPublicUrl(appPackageJson); - const servedUrl = - envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/'); - return ensureSlash(servedUrl, true); -} - -const moduleFileExtensions = [ - 'web.mjs', - 'mjs', - 'web.js', - 'js', - 'web.ts', - 'ts', - 'web.tsx', - 'tsx', - 'json', - 'web.jsx', - 'jsx', -]; - -// Resolve file paths in the order expected by the test tooling. -const resolveModule = (resolveFn, filePath) => { - const extension = moduleFileExtensions.find(extension => - fs.existsSync(resolveFn(`${filePath}.${extension}`)) - ); - - if (extension) { - return resolveFn(`${filePath}.${extension}`); - } - - return resolveFn(`${filePath}.js`); -}; - -// config after eject: we're in ./config/ -module.exports = { - dotenv: resolveApp('.env'), - appPath: resolveApp('.'), - appBuild: resolveApp('build'), - appPublic: resolveApp('public'), - appHtml: resolveApp('index.html'), - appIndexJs: resolveModule(resolveApp, 'src/index'), - appPackageJson: resolveApp('package.json'), - appSrc: resolveApp('src'), - appTsConfig: resolveApp('tsconfig.json'), - appJsConfig: resolveApp('jsconfig.json'), - yarnLockFile: resolveApp('yarn.lock'), - testsSetup: resolveModule(resolveApp, 'src/setupTests'), - proxySetup: resolveApp('src/setupProxy.js'), - appNodeModules: resolveApp('node_modules'), - publicUrl: getPublicUrl(resolveApp('package.json')), - servedPath: getServedPath(resolveApp('package.json')), -}; - - - -module.exports.moduleFileExtensions = moduleFileExtensions; diff --git a/client/eslint.config.cjs b/client/eslint.config.cjs new file mode 100644 index 0000000..cdf94b2 --- /dev/null +++ b/client/eslint.config.cjs @@ -0,0 +1,62 @@ +const js = require('@eslint/js'); +const globals = require('globals'); +const importPlugin = require('eslint-plugin-import'); +const jsxA11y = require('eslint-plugin-jsx-a11y'); +const react = require('eslint-plugin-react'); +const reactHooks = require('eslint-plugin-react-hooks'); + +module.exports = [ + { + ignores: ['build/**'], + }, + js.configs.recommended, + { + files: ['src/**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 'latest', + globals: { + ...globals.browser, + ...globals.jest, + module: 'readonly', + process: 'readonly', + }, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + sourceType: 'module', + }, + plugins: { + import: importPlugin, + 'jsx-a11y': jsxA11y, + react, + 'react-hooks': reactHooks, + }, + rules: { + ...importPlugin.configs.recommended.rules, + ...jsxA11y.configs.recommended.rules, + ...react.configs.recommended.rules, + 'import/no-unresolved': ['error', { ignore: ['^react-ga4$'] }], + 'react-hooks/exhaustive-deps': 'warn', + 'react-hooks/rules-of-hooks': 'error', + 'no-console': 'warn', + 'no-underscore-dangle': 'off', + 'jsx-a11y/no-autofocus': 'off', + 'react/jsx-filename-extension': ['warn', { extensions: ['.js', '.jsx'] }], + 'react/jsx-props-no-spreading': ['warn', { custom: 'ignore' }], + 'react/react-in-jsx-scope': 'off', + semi: ['warn', 'always'], + }, + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.jsx', '.mjs'], + }, + }, + react: { + version: 'detect', + }, + }, + }, +]; diff --git a/client/jest.config.cjs b/client/jest.config.cjs new file mode 100644 index 0000000..9797e12 --- /dev/null +++ b/client/jest.config.cjs @@ -0,0 +1,14 @@ +module.exports = { + clearMocks: true, + collectCoverageFrom: ['src/**/*.{js,jsx}', '!src/custom-service-worker.js'], + moduleNameMapper: { + '\\.(css|less|sass|scss)$': 'identity-obj-proxy', + '\\.(mp3|ogg)$': '/test/fileMock.js', + }, + roots: ['/src'], + setupFilesAfterEnv: ['/src/setupTests.js'], + testEnvironment: 'jsdom', + transform: { + '^.+\\.[jt]sx?$': 'babel-jest', + }, +}; diff --git a/client/package.json b/client/package.json index eb1d2cc..9cdc5f6 100644 --- a/client/package.json +++ b/client/package.json @@ -3,47 +3,43 @@ "version": "2.0.0", "private": true, "dependencies": { - "@material-ui/core": "^4.11.2", - "@material-ui/icons": "^4.11.2", - "@material-ui/lab": "4.0.0-alpha.46", - "@material-ui/styles": "^4.11.5", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^6.5.0", + "@mui/material": "^6.5.0", + "@mui/styles": "^6.5.0", "clsx": "1.1.0", - "connected-react-router": "6.8.0", - "cubing": "0.63.3", "date-fns": "^2.16.1", "history": "4.10.1", - "material-ui-confirm": "2.0.4", - "prop-types": "15.7.2", + "material-ui-confirm": "^4.0.0", + "prop-types": "15.8.1", "qs": "^6.9.6", - "react": "^16.14.0", - "react-dom": "^16.14.0", - "react-ga": "2.7.0", + "react": "18.3.1", + "react-cube-svg": "^0.2.3", + "react-dom": "18.3.1", + "react-ga4": "^3.0.1", "react-markdown": "6.0.3", - "react-redux": "^7.2.2", - "react-router-dom": "^5.2.0", - "react-twitch-embed": "^2.1.0", - "react-virtualized": "^9.22.3", - "redux": "^4.0.5", - "redux-thunk": "^2.3.0", + "react-redux": "^8.1.3", + "react-router-dom": "^5.3.4", + "react-twitch-embed": "^3.0.2", + "react-virtualized": "^9.22.6", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", "reselect": "^4.0.0", "socket.io-client": "^4.8.3", "stackmat-signal-processor": "^0.1.0", "uifx": "^2.0.7", - "letscube-scrambles": "0.1.0", "workbox-window": "^7.4.1" }, "scripts": { "start": "vite", "build": "vite build", "preview": "vite preview", - "test": "node scripts/test.js", - "test:ci": "CI=true node scripts/test.js --watchAll=false --passWithNoTests", + "test": "jest", + "test:ci": "jest --ci --passWithNoTests", "lint": "eslint ./src/", "lint:fix": "eslint --fix ./src/" }, - "eslintConfig": { - "extends": "react-app" - }, "browserslist": { "production": [ ">0.2%", @@ -56,96 +52,28 @@ "last 1 safari version" ] }, - "jest": { - "roots": [ - "/src" - ], - "collectCoverageFrom": [ - "src/**/*.{js,jsx,ts,tsx}", - "!src/**/*.d.ts" - ], - "setupFiles": [ - "react-app-polyfill/jsdom" - ], - "setupFilesAfterEnv": [ - "/src/setupTests.js" - ], - "testMatch": [ - "/src/**/__tests__/**/*.{js,jsx,ts,tsx}", - "/src/**/*.{spec,test}.{js,jsx,ts,tsx}" - ], - "testEnvironment": "jest-environment-jsdom-fourteen", - "transform": { - "^.+\\.(js|jsx|ts|tsx)$": "/node_modules/babel-jest", - "^.+\\.css$": "/config/jest/cssTransform.js", - "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "/config/jest/fileTransform.js" - }, - "transformIgnorePatterns": [ - "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$", - "^.+\\.module\\.(css|sass|scss)$" - ], - "modulePaths": [ - "/usr/lib/nodejs", - "/usr/lib/node_modules", - "/usr/share/javascript" - ], - "moduleNameMapper": { - "^react-native$": "react-native-web", - "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy" - }, - "moduleFileExtensions": [ - "web.js", - "js", - "web.ts", - "ts", - "web.tsx", - "tsx", - "json", - "web.jsx", - "jsx", - "node" - ], - "watchPlugins": [ - "jest-watch-typeahead/filename", - "jest-watch-typeahead/testname" - ] - }, - "babel": { - "presets": [ - "react-app" - ] - }, "devDependencies": { - "@babel/core": "7.7.4", - "@testing-library/jest-dom": "4.2.4", - "@testing-library/react": "9.4.0", - "@testing-library/user-event": "7.2.1", - "@typescript-eslint/eslint-plugin": "^2.34.0", - "@typescript-eslint/parser": "^2.34.0", + "@babel/core": "^7.28.0", + "@babel/preset-env": "^7.28.0", + "@babel/preset-react": "^7.27.1", + "@eslint/js": "^9.39.4", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^18.3.28", + "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "6.0.3", - "babel-eslint": "10.0.3", - "babel-jest": "^24.9.0", - "babel-preset-react-app": "^9.1.2", - "camelcase": "^5.3.1", - "dotenv": "8.2.0", - "dotenv-expand": "5.1.0", - "enzyme": "3.11.0", - "enzyme-adapter-react-16": "1.15.2", - "eslint": "^6.6.0", - "eslint-config-airbnb": "18.1.0", - "eslint-config-react-app": "^5.2.1", - "eslint-plugin-flowtype": "3.13.0", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jsx-a11y": "6.2.3", - "eslint-plugin-prettier": "3.1.2", + "babel-jest": "^30.4.1", + "eslint": "^9.39.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^1.6.1", + "eslint-plugin-react-hooks": "^7.1.1", + "globals": "^17.7.0", "identity-obj-proxy": "3.0.0", - "jest": "^26.6.3", - "jest-environment-jsdom-fourteen": "^1.0.1", - "jest-resolve": "^26.6.2", - "jest-watch-typeahead": "^0.6.1", - "react-app-polyfill": "^1.0.6", + "jest": "^30.4.2", + "jest-environment-jsdom": "^30.4.1", "vite": "8.1.4", "vite-plugin-pwa": "1.3.0", "workbox-build": "^7.4.1", diff --git a/client/scripts/test.js b/client/scripts/test.js deleted file mode 100644 index b57cb38..0000000 --- a/client/scripts/test.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -// Do this as the first thing so that any code reading it knows the right env. -process.env.BABEL_ENV = 'test'; -process.env.NODE_ENV = 'test'; -process.env.PUBLIC_URL = ''; - -// Makes the script crash on unhandled rejections instead of silently -// ignoring them. In the future, promise rejections that are not handled will -// terminate the Node.js process with a non-zero exit code. -process.on('unhandledRejection', err => { - throw err; -}); - -// Ensure environment variables are read. -require('../config/env'); - - -const jest = require('jest'); -const execSync = require('child_process').execSync; -let argv = process.argv.slice(2); - -function isInGitRepository() { - try { - execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); - return true; - } catch (e) { - return false; - } -} - -function isInMercurialRepository() { - try { - execSync('hg --cwd . root', { stdio: 'ignore' }); - return true; - } catch (e) { - return false; - } -} - -// Watch unless on CI or explicitly running all tests -if ( - !process.env.CI && - argv.indexOf('--watchAll') === -1 && - argv.indexOf('--watchAll=false') === -1 -) { - // https://github.com/facebook/create-react-app/issues/5210 - const hasSourceControl = isInGitRepository() || isInMercurialRepository(); - argv.push(hasSourceControl ? '--watch' : '--watchAll'); -} - - -jest.run(argv); diff --git a/client/src/components/AccountMenu.jsx b/client/src/components/AccountMenu.jsx index ca33f62..0b84652 100644 --- a/client/src/components/AccountMenu.jsx +++ b/client/src/components/AccountMenu.jsx @@ -1,21 +1,22 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; -import { makeStyles, useTheme } from '@material-ui/core/styles'; -import useMediaQuery from '@material-ui/core/useMediaQuery'; -import Avatar from '@material-ui/core/Avatar'; -import Divider from '@material-ui/core/Divider'; -import Drawer from '@material-ui/core/Drawer'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import ListItemText from '@material-ui/core/ListItemText'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import Typography from '@material-ui/core/Typography'; -import ExitToAppIcon from '@material-ui/icons/ExitToApp'; -import PeopleIcon from '@material-ui/icons/People'; -import PersonIcon from '@material-ui/icons/Person'; +import { makeStyles } from '@mui/styles'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import Avatar from '@mui/material/Avatar'; +import Divider from '@mui/material/Divider'; +import Drawer from '@mui/material/Drawer'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import Typography from '@mui/material/Typography'; +import ExitToAppIcon from '@mui/icons-material/ExitToApp'; +import PeopleIcon from '@mui/icons-material/People'; +import PersonIcon from '@mui/icons-material/Person'; import { isFeatureEnabled } from '../lib/features'; const useStyles = makeStyles((theme) => ({ diff --git a/client/src/components/Admin/RoomCard.jsx b/client/src/components/Admin/RoomCard.jsx index 6dc0fd2..2e5218c 100644 --- a/client/src/components/Admin/RoomCard.jsx +++ b/client/src/components/Admin/RoomCard.jsx @@ -1,15 +1,15 @@ import React, { useState, useRef } from 'react'; import { useDispatch } from 'react-redux'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles } from '@mui/styles'; import PropTypes from 'prop-types'; -import Card from '@material-ui/core/Card'; -import CardContent from '@material-ui/core/CardContent'; -import CardHeader from '@material-ui/core/CardHeader'; -import Typography from '@material-ui/core/Typography'; -import IconButton from '@material-ui/core/IconButton'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; import { useConfirm } from 'material-ui-confirm'; import { deleteRoom } from '../../store/room/actions'; import { createMessage } from '../../store/messages/actions'; diff --git a/client/src/components/Admin/index.jsx b/client/src/components/Admin/index.jsx index 6817656..abd0832 100644 --- a/client/src/components/Admin/index.jsx +++ b/client/src/components/Admin/index.jsx @@ -1,10 +1,10 @@ import React, { useEffect } from 'react'; import { useDispatch, connect } from 'react-redux'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles } from '@mui/styles'; import PropTypes from 'prop-types'; -import Container from '@material-ui/core/Container'; -import Paper from '@material-ui/core/Paper'; -import Button from '@material-ui/core/Button'; +import Container from '@mui/material/Container'; +import Paper from '@mui/material/Paper'; +import Button from '@mui/material/Button'; import { fetchAdminData } from '../../store/admin/actions'; import RoomCard from './RoomCard'; diff --git a/client/src/components/App.jsx b/client/src/components/App.jsx index f9b3e18..300edae 100644 --- a/client/src/components/App.jsx +++ b/client/src/components/App.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { Provider } from 'react-redux'; -import { ConnectedRouter } from 'connected-react-router'; -import CssBaseline from '@material-ui/core/CssBaseline'; +import { Router } from 'react-router-dom'; +import CssBaseline from '@mui/material/CssBaseline'; import { ConfirmProvider } from 'material-ui-confirm'; import { ThemeProvider } from '../theme'; import store from '../store'; @@ -11,14 +11,14 @@ import Navigation from './Navigation'; function App() { return ( - + - + ); } diff --git a/client/src/components/App.test.jsx b/client/src/components/App.test.jsx index b08ffa2..ef6d65a 100644 --- a/client/src/components/App.test.jsx +++ b/client/src/components/App.test.jsx @@ -1,7 +1,21 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import App from './App'; +jest.mock('react-ga4', () => ({ + __esModule: true, + default: { + initialize: jest.fn(), + send: jest.fn(), + }, +})); + +jest.mock('./Navigation', () => function MockNavigation() { + return
; +}); + it('renders without crashing', () => { - shallow(); + render(); + + expect(screen.getByTestId('navigation')).toBeInTheDocument(); }); diff --git a/client/src/components/Footer.jsx b/client/src/components/Footer.jsx index 83980d0..045ef52 100644 --- a/client/src/components/Footer.jsx +++ b/client/src/components/Footer.jsx @@ -2,18 +2,19 @@ import React, { useState } from 'react'; import { connect } from 'react-redux'; import { Link as ReactRouterLink } from 'react-router-dom'; import PropTypes from 'prop-types'; -import { makeStyles, useTheme } from '@material-ui/core/styles'; -import Grid from '@material-ui/core/Grid'; -import Paper from '@material-ui/core/Paper'; -import Typography from '@material-ui/core/Typography'; -import Link from '@material-ui/core/Link'; -import Popover from '@material-ui/core/Popover'; -import Tooltip from '@material-ui/core/Tooltip'; -import IconButton from '@material-ui/core/IconButton'; -import EmojiObjectsIcon from '@material-ui/icons/EmojiObjects'; -import EmojiObjectsOutlinedIcon from '@material-ui/icons/EmojiObjectsOutlined'; -import CloudIcon from '@material-ui/icons/Cloud'; -import CloudOffIcon from '@material-ui/icons/CloudOff'; +import { useTheme } from '@mui/material/styles'; +import { makeStyles } from '@mui/styles'; +import Grid from '@mui/material/Grid'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import Link from '@mui/material/Link'; +import Popover from '@mui/material/Popover'; +import Tooltip from '@mui/material/Tooltip'; +import IconButton from '@mui/material/IconButton'; +import EmojiObjectsIcon from '@mui/icons-material/EmojiObjects'; +import EmojiObjectsOutlinedIcon from '@mui/icons-material/EmojiObjectsOutlined'; +import CloudIcon from '@mui/icons-material/Cloud'; +import CloudOffIcon from '@mui/icons-material/CloudOff'; import { version } from '../../package.json'; import { useToggleTheme } from '../theme'; @@ -86,7 +87,7 @@ function Footer({ onClick={toggleTheme} aria-label="Toggle theme" > - { theme.palette.type === 'dark' + { theme.palette.mode === 'dark' ? : } diff --git a/client/src/components/Friends.jsx b/client/src/components/Friends.jsx index d013df8..ad26c3e 100644 --- a/client/src/components/Friends.jsx +++ b/client/src/components/Friends.jsx @@ -4,23 +4,23 @@ import React, { import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; import { useDispatch } from 'react-redux'; -import Avatar from '@material-ui/core/Avatar'; -import Button from '@material-ui/core/Button'; -import CircularProgress from '@material-ui/core/CircularProgress'; -import Container from '@material-ui/core/Container'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import Divider from '@material-ui/core/Divider'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemAvatar from '@material-ui/core/ListItemAvatar'; -import ListItemText from '@material-ui/core/ListItemText'; -import Paper from '@material-ui/core/Paper'; -import TextField from '@material-ui/core/TextField'; -import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import Container from '@mui/material/Container'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import Divider from '@mui/material/Divider'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import ListItemText from '@mui/material/ListItemText'; +import Paper from '@mui/material/Paper'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { makeStyles } from '@mui/styles'; import { lcFetch } from '../lib/fetch'; import createRequestSequence from '../lib/requestSequence'; import { createRoom } from '../store/rooms/actions'; @@ -70,7 +70,7 @@ const errorMessage = async (response, fallback) => { try { const body = await response.json(); return body.message || fallback; - } catch (error) { + } catch { return fallback; } }; diff --git a/client/src/components/Header.jsx b/client/src/components/Header.jsx index 0417f60..ed28121 100644 --- a/client/src/components/Header.jsx +++ b/client/src/components/Header.jsx @@ -2,19 +2,19 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; -import { makeStyles } from '@material-ui/core/styles'; -import Grid from '@material-ui/core/Grid'; -import AppBar from '@material-ui/core/AppBar'; -import Typography from '@material-ui/core/Typography'; -import Toolbar from '@material-ui/core/Toolbar'; -import IconButton from '@material-ui/core/IconButton'; -import Avatar from '@material-ui/core/Avatar'; -import Button from '@material-ui/core/Button'; -import Badge from '@material-ui/core/Badge'; +import { makeStyles } from '@mui/styles'; +import Grid from '@mui/material/Grid'; +import AppBar from '@mui/material/AppBar'; +import Typography from '@mui/material/Typography'; +import Toolbar from '@mui/material/Toolbar'; +import IconButton from '@mui/material/IconButton'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import Badge from '@mui/material/Badge'; import { apiOrigin } from '../lib/fetch'; import { getNameFromId } from '../lib/events'; import { getWcaAuthorizationUrl } from '../lib/wcaAuth'; -import NotificationsIcon from '@material-ui/icons/Notifications'; +import NotificationsIcon from '@mui/icons-material/Notifications'; import AccountMenu from './AccountMenu'; import NotificationPopover from './Notifications/NotificationPopover'; diff --git a/client/src/components/Lobby/Announcements.jsx b/client/src/components/Lobby/Announcements.jsx index 1d656f6..c8ee080 100644 --- a/client/src/components/Lobby/Announcements.jsx +++ b/client/src/components/Lobby/Announcements.jsx @@ -1,7 +1,7 @@ import React from 'react'; import ReactMarkdown from 'react-markdown'; -import { makeStyles } from '@material-ui/core/styles'; -import Paper from '@material-ui/core/Paper'; +import { makeStyles } from '@mui/styles'; +import Paper from '@mui/material/Paper'; import { lcFetch } from '../../lib/fetch'; const useStyles = makeStyles({ diff --git a/client/src/components/Lobby/Announcements.test.jsx b/client/src/components/Lobby/Announcements.test.jsx index 1161ea0..38d5b7b 100644 --- a/client/src/components/Lobby/Announcements.test.jsx +++ b/client/src/components/Lobby/Announcements.test.jsx @@ -1,6 +1,5 @@ import React from 'react'; -import { mount } from 'enzyme'; -import { act } from 'react-dom/test-utils'; +import { render, screen } from '@testing-library/react'; import Announcements from './Announcements'; import { lcFetch } from '../../lib/fetch'; @@ -8,8 +7,6 @@ jest.mock('../../lib/fetch', () => ({ lcFetch: jest.fn(), })); -const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); - describe('Announcements', () => { beforeEach(() => { lcFetch.mockReset(); @@ -21,19 +18,10 @@ describe('Announcements', () => { text: jest.fn().mockResolvedValue('[Docs](https://example.com)'), }); - let wrapper; - - await act(async () => { - wrapper = mount(); - await flushPromises(); - }); - - wrapper.update(); - - const link = wrapper.find('a').first(); + render(); - expect(link.text()).toEqual('Docs'); - expect(link.prop('href')).toEqual('https://example.com'); - expect(link.prop('target')).toEqual('_blank'); + const link = await screen.findByRole('link', { name: 'Docs' }); + expect(link).toHaveAttribute('href', 'https://example.com'); + expect(link).toHaveAttribute('target', '_blank'); }); }); diff --git a/client/src/components/Lobby/UserList.jsx b/client/src/components/Lobby/UserList.jsx index a8bd6df..8adde81 100644 --- a/client/src/components/Lobby/UserList.jsx +++ b/client/src/components/Lobby/UserList.jsx @@ -1,14 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import Paper from '@material-ui/core/Paper'; -import Toolbar from '@material-ui/core/Toolbar'; -// import Divider from '@material-ui/core/Divider'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemText from '@material-ui/core/ListItemText'; -import ListItemAvatar from '@material-ui/core/ListItemAvatar'; -import Avatar from '@material-ui/core/Avatar'; +import { makeStyles } from '@mui/styles'; +import Paper from '@mui/material/Paper'; +import Toolbar from '@mui/material/Toolbar'; +// import Divider from '@mui/material/Divider'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import Avatar from '@mui/material/Avatar'; import { Link } from 'react-router-dom'; const useStyles = makeStyles(() => ({ diff --git a/client/src/components/Lobby/index.jsx b/client/src/components/Lobby/index.jsx index d8720dc..ef8f480 100644 --- a/client/src/components/Lobby/index.jsx +++ b/client/src/components/Lobby/index.jsx @@ -3,22 +3,20 @@ import { connect, useDispatch } from 'react-redux'; import { Link } from 'react-router-dom'; import clsx from 'clsx'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import Paper from '@material-ui/core/Paper'; -import Container from '@material-ui/core/Container'; -import Grid from '@material-ui/core/Grid'; -import Divider from '@material-ui/core/Divider'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import Button from '@material-ui/core/Button'; -import Alert from '@material-ui/lab/Alert'; -import ListSubheader from '@material-ui/core/ListSubheader'; -import BottomNavigation from '@material-ui/core/BottomNavigation'; -import BottomNavigationAction from '@material-ui/core/BottomNavigationAction'; -import AddIcon from '@material-ui/icons/Add'; -import DynamicFeedIcon from '@material-ui/icons/DynamicFeed'; -import PeopleIcon from '@material-ui/icons/People'; +import { makeStyles } from '@mui/styles'; +import Paper from '@mui/material/Paper'; +import Container from '@mui/material/Container'; +import Grid from '@mui/material/Grid'; +import Divider from '@mui/material/Divider'; +import List from '@mui/material/List'; +import Button from '@mui/material/Button'; +import Alert from '@mui/material/Alert'; +import ListSubheader from '@mui/material/ListSubheader'; +import BottomNavigation from '@mui/material/BottomNavigation'; +import BottomNavigationAction from '@mui/material/BottomNavigationAction'; +import AddIcon from '@mui/icons-material/Add'; +import DynamicFeedIcon from '@mui/icons-material/DynamicFeed'; +import PeopleIcon from '@mui/icons-material/People'; import RoomConfigureDialog from '../RoomConfigureDialog'; import RoomListItem from '../RoomListItem'; import EventListItem from '../common/EventListItem'; @@ -56,7 +54,7 @@ const useStyles = makeStyles((theme) => ({ display: 'flex', flexDirection: 'column', flexGrow: 1, - height: 0, + minHeight: 0, overflowY: 'auto', }, eventList: { @@ -172,19 +170,16 @@ function Lobby({ )} {user.id && ( - } onClick={() => setCreateRoomDialogOpen(true)} > - - - Create Room - + )}
diff --git a/client/src/components/Navigation.jsx b/client/src/components/Navigation.jsx index 4577781..048a2ed 100644 --- a/client/src/components/Navigation.jsx +++ b/client/src/components/Navigation.jsx @@ -4,13 +4,13 @@ import { Switch, Route, Redirect, useLocation, } from 'react-router-dom'; import { connect, useDispatch } from 'react-redux'; -import { push } from 'connected-react-router'; -// import Backdrop from '@material-ui/core/Backdrop'; -import CircularProgress from '@material-ui/core/CircularProgress'; -import Button from '@material-ui/core/Button'; -import Snackbar from '@material-ui/core/Snackbar'; -import Alert from '@material-ui/lab/Alert'; -import { makeStyles } from '@material-ui/core/styles'; +import history from '../lib/history'; +// import Backdrop from '@mui/material/Backdrop'; +import CircularProgress from '@mui/material/CircularProgress'; +import Button from '@mui/material/Button'; +import Snackbar from '@mui/material/Snackbar'; +import Alert from '@mui/material/Alert'; +import { makeStyles } from '@mui/styles'; import PrivateRoute from './common/PrivateRoute'; import Header from './Header'; import Footer from './Footer'; @@ -215,7 +215,7 @@ function Navigation({ atPendingRoom={atPendingRoom} error={resultSubmission.error} onDiscard={() => dispatch(discardPendingResult(pendingResult.submissionId))} - onReturn={() => dispatch(push(pendingRoomPath))} + onReturn={() => history.push(pendingRoomPath)} pendingResult={pendingResult} privateRoom={!!room.private} status={resultSubmission.status} @@ -274,6 +274,7 @@ Navigation.propTypes = { _id: PropTypes.string, accessCode: PropTypes.string, fetching: PropTypes.bool, + private: PropTypes.bool, type: PropTypes.string, resultSubmission: PropTypes.shape({ status: PropTypes.oneOf(['idle', 'pending', 'sending', 'failed']), diff --git a/client/src/components/Navigation.test.jsx b/client/src/components/Navigation.test.jsx index 984b4f0..78348b1 100644 --- a/client/src/components/Navigation.test.jsx +++ b/client/src/components/Navigation.test.jsx @@ -1,7 +1,5 @@ import React from 'react'; -import { shallow } from 'enzyme'; -import Alert from '@material-ui/lab/Alert'; -import Button from '@material-ui/core/Button'; +import { fireEvent, render, screen } from '@testing-library/react'; import { GlobalPendingResultAlert, shouldShowGlobalPendingResult, @@ -39,7 +37,7 @@ describe('global pending result alert', () => { it('keeps discard accessible when the pending room could not be joined', () => { const onDiscard = jest.fn(); - const wrapper = shallow( + render( { userId={42} />, ); - const actions = shallow(
{wrapper.find(Alert).prop('action')}
); - expect(wrapper.text()).toContain('Enter the room password below'); - expect(actions.find(Button)).toHaveLength(1); - expect(actions.find(Button).text()).toBe('Discard saved result'); - - actions.find(Button).simulate('click'); + expect(screen.getByText(/Enter the room password below/)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Discard saved result' })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: 'Discard saved result' })); expect(onDiscard).toHaveBeenCalledTimes(1); }); it('offers a return action when the user is somewhere else', () => { const onReturn = jest.fn(); - const wrapper = shallow( + render( { userId={42} />, ); - const actions = shallow(
{wrapper.find(Alert).prop('action')}
); - expect(actions.find(Button).map((button) => button.text())).toEqual([ - 'Return to room', - 'Discard saved result', - ]); - actions.find(Button).at(0).simulate('click'); + fireEvent.click(screen.getByRole('button', { name: 'Return to room' })); expect(onReturn).toHaveBeenCalledTimes(1); + expect(screen.getByRole('button', { name: 'Discard saved result' })).toBeInTheDocument(); }); it('does not offer discard after delivery has started', () => { - const wrapper = shallow( + render( { userId={42} />, ); - const actions = shallow(
{wrapper.find(Alert).prop('action')}
); - expect(actions.find(Button)).toHaveLength(1); - expect(actions.find(Button).text()).toBe('Return to room'); - expect(wrapper.text()).toContain('finish submitting'); + expect(screen.getByRole('button', { name: 'Return to room' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Discard saved result' })).not.toBeInTheDocument(); + expect(screen.getByText(/finish submitting/)).toBeInTheDocument(); }); }); diff --git a/client/src/components/Notifications/NotificationList.jsx b/client/src/components/Notifications/NotificationList.jsx index 5700adf..aa78988 100644 --- a/client/src/components/Notifications/NotificationList.jsx +++ b/client/src/components/Notifications/NotificationList.jsx @@ -1,11 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemText from '@material-ui/core/ListItemText'; -import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; +import Button from '@mui/material/Button'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import Typography from '@mui/material/Typography'; +import { makeStyles } from '@mui/styles'; import notificationPresentation from './registry'; const useStyles = makeStyles((theme) => ({ diff --git a/client/src/components/Notifications/NotificationPopover.jsx b/client/src/components/Notifications/NotificationPopover.jsx index 7bd32e7..10af7cb 100644 --- a/client/src/components/Notifications/NotificationPopover.jsx +++ b/client/src/components/Notifications/NotificationPopover.jsx @@ -1,17 +1,17 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; -import { makeStyles } from '@material-ui/core/styles'; -import Avatar from '@material-ui/core/Avatar'; -import Button from '@material-ui/core/Button'; -import Divider from '@material-ui/core/Divider'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemAvatar from '@material-ui/core/ListItemAvatar'; -import ListItemText from '@material-ui/core/ListItemText'; -import Popover from '@material-ui/core/Popover'; -import Typography from '@material-ui/core/Typography'; -import NotificationsNoneIcon from '@material-ui/icons/NotificationsNone'; +import { makeStyles } from '@mui/styles'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import Divider from '@mui/material/Divider'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import ListItemText from '@mui/material/ListItemText'; +import Popover from '@mui/material/Popover'; +import Typography from '@mui/material/Typography'; +import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone'; import notificationPresentation from './registry'; const useStyles = makeStyles((theme) => ({ diff --git a/client/src/components/Notifications/index.jsx b/client/src/components/Notifications/index.jsx index 69fda0b..ede6833 100644 --- a/client/src/components/Notifications/index.jsx +++ b/client/src/components/Notifications/index.jsx @@ -1,11 +1,11 @@ import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; -import CircularProgress from '@material-ui/core/CircularProgress'; -import Container from '@material-ui/core/Container'; -import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import Container from '@mui/material/Container'; +import Typography from '@mui/material/Typography'; +import { makeStyles } from '@mui/styles'; import { fetchNotifications, markAllNotificationsRead, diff --git a/client/src/components/PublicProfile.jsx b/client/src/components/PublicProfile.jsx index 727d2fe..1d6fece 100644 --- a/client/src/components/PublicProfile.jsx +++ b/client/src/components/PublicProfile.jsx @@ -2,13 +2,13 @@ import React, { useCallback, useEffect, useRef, useState, } from 'react'; import { Link, useParams } from 'react-router-dom'; -import Avatar from '@material-ui/core/Avatar'; -import Button from '@material-ui/core/Button'; -import CircularProgress from '@material-ui/core/CircularProgress'; -import Container from '@material-ui/core/Container'; -import Paper from '@material-ui/core/Paper'; -import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import Container from '@mui/material/Container'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import { makeStyles } from '@mui/styles'; import { lcFetch } from '../lib/fetch'; import { isFeatureEnabled } from '../lib/features'; diff --git a/client/src/components/Room/Common/AdminToolbar.jsx b/client/src/components/Room/Common/AdminToolbar.jsx index 200d3a5..e0eeda3 100644 --- a/client/src/components/Room/Common/AdminToolbar.jsx +++ b/client/src/components/Room/Common/AdminToolbar.jsx @@ -1,15 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles } from '@mui/styles'; import { connect, useDispatch } from 'react-redux'; -import Toolbar from '@material-ui/core/Toolbar'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import Button from '@material-ui/core/Button'; -import FormGroup from '@material-ui/core/FormGroup'; -import Select from '@material-ui/core/Select'; -import ListSubheader from '@material-ui/core/ListSubheader'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; +import Toolbar from '@mui/material/Toolbar'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import Button from '@mui/material/Button'; +import FormGroup from '@mui/material/FormGroup'; +import Select from '@mui/material/Select'; +import ListSubheader from '@mui/material/ListSubheader'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; import { useConfirm } from 'material-ui-confirm'; import { deleteRoom, diff --git a/client/src/components/Room/Common/EditDialog.jsx b/client/src/components/Room/Common/EditDialog.jsx index 86c4bf8..fcc4756 100644 --- a/client/src/components/Room/Common/EditDialog.jsx +++ b/client/src/components/Room/Common/EditDialog.jsx @@ -1,13 +1,13 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import DialogContent from '@material-ui/core/DialogContent'; -import TextField from '@material-ui/core/TextField'; -import Button from '@material-ui/core/Button'; -import Checkbox from '@material-ui/core/Checkbox'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import TextField from '@mui/material/TextField'; +import Button from '@mui/material/Button'; +import Checkbox from '@mui/material/Checkbox'; +import FormControlLabel from '@mui/material/FormControlLabel'; import { formatRawTime, parseTime } from '../../../lib/utils'; function EditDialog({ diff --git a/client/src/components/Room/Common/Login.jsx b/client/src/components/Room/Common/Login.jsx index abc29d1..a460aa2 100644 --- a/client/src/components/Room/Common/Login.jsx +++ b/client/src/components/Room/Common/Login.jsx @@ -1,12 +1,12 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles } from '@mui/styles'; import { connect } from 'react-redux'; -import Paper from '@material-ui/core/Paper'; -import TextField from '@material-ui/core/TextField'; -import Button from '@material-ui/core/Button'; -import IconButton from '@material-ui/core/IconButton'; -import ArrowBackIcon from '@material-ui/icons/ArrowBack'; +import Paper from '@mui/material/Paper'; +import TextField from '@mui/material/TextField'; +import Button from '@mui/material/Button'; +import IconButton from '@mui/material/IconButton'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { Link } from 'react-router-dom'; import { joinRoom, diff --git a/client/src/components/Room/Common/Main.jsx b/client/src/components/Room/Common/Main.jsx index 44fd4bf..50885a7 100644 --- a/client/src/components/Room/Common/Main.jsx +++ b/client/src/components/Room/Common/Main.jsx @@ -1,16 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; +import { withStyles } from '@mui/styles'; import { connect } from 'react-redux'; -import Grid from '@material-ui/core/Grid'; -import Paper from '@material-ui/core/Paper'; -import Divider from '@material-ui/core/Divider'; -import Typography from '@material-ui/core/Typography'; -import Button from '@material-ui/core/Button'; -import ClickAwayListener from '@material-ui/core/ClickAwayListener'; -import Alert from '@material-ui/lab/Alert'; +import Grid from '@mui/material/Grid'; +import Paper from '@mui/material/Paper'; +import Divider from '@mui/material/Divider'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import Alert from '@mui/material/Alert'; import UIfx from 'uifx'; -import { push } from 'connected-react-router'; +import history from '../../../lib/history'; import notificationAsset from '../../../assets/notification.mp3'; import calcStats from '../../../lib/stats'; import { @@ -34,7 +34,7 @@ import Scramble from '../../common/Scramble'; import ScramblePreview from '../../common/ScramblePreview'; import UserStats from './UserStats'; -const useStyles = withStyles((theme) => ({ +const withComponentStyles = withStyles((theme) => ({ root: { display: 'flex', flexGrow: 1, @@ -185,7 +185,7 @@ export class Main extends React.Component { @@ -374,4 +374,4 @@ const mapStateToProps = (state) => ({ user: state.user, }); -export default connect(mapStateToProps)(useStyles(Main)); +export default connect(mapStateToProps)(withComponentStyles(Main)); diff --git a/client/src/components/Room/Common/Main.test.jsx b/client/src/components/Room/Common/Main.test.jsx index cd61307..cb650fe 100644 --- a/client/src/components/Room/Common/Main.test.jsx +++ b/client/src/components/Room/Common/Main.test.jsx @@ -1,7 +1,6 @@ import React from 'react'; -import { shallow } from 'enzyme'; -import Alert from '@material-ui/lab/Alert'; -import Button from '@material-ui/core/Button'; +import Alert from '@mui/material/Alert'; +import Button from '@mui/material/Button'; import Timer from '../../Timer/index'; import ScramblePreview from '../../common/ScramblePreview'; import { Main } from './Main'; @@ -27,6 +26,13 @@ const pendingResult = createPendingResult({ now: () => 1000, }); +const findElements = (node, type) => { + if (!React.isValidElement(node)) return []; + const children = React.Children.toArray(node.props.children) + .flatMap((child) => findElements(child, type)); + return node.type === type ? [node, ...children] : children; +}; + const makeProps = (overrides = {}) => ({ classes: { root: 'root', @@ -60,14 +66,22 @@ const makeProps = (overrides = {}) => ({ ...overrides, }); +const createMain = (props) => { + const main = new Main(props); + main.setState = (update) => { + main.state = { ...main.state, ...update }; + }; + return main; +}; + describe('room pending result UX', () => { it('shows scramble previews for non-3x3 events', () => { const props = makeProps({ room: { ...makeProps().room, event: 'pyram' }, }); - const wrapper = shallow(
); + const previews = findElements(createMain(props).render(), ScramblePreview); - expect(wrapper.find(ScramblePreview).props()).toMatchObject({ + expect(previews[0].props).toMatchObject({ event: 'pyram', scramble: 'R U', }); @@ -87,18 +101,19 @@ describe('room pending result UX', () => { }, }, }); - const wrapper = shallow(
); - wrapper.instance().handlePriming(); - wrapper.setProps({ + const main = createMain(props); + main.handlePriming(); + main.props = { + ...props, room: { ...props.room, attempts: [{ _id: 'replacement-attempt', id: 0, scrambles: ['F R'], results: {}, }], }, - }); + }; - wrapper.instance().onSubmitTime({ time: 1234, penalties: {} }); + main.onSubmitTime({ time: 1234, penalties: {} }); expect(props.dispatch).toHaveBeenCalledWith({ type: SUBMIT_RESULT, @@ -122,25 +137,25 @@ describe('room pending result UX', () => { }, }, }); - const wrapper = shallow(
); + const tree = createMain(props).render(); - expect(wrapper.find(Timer).prop('disabled')).toBe(false); - expect(wrapper.find(Alert)).toHaveLength(0); + expect(findElements(tree, Timer)[0].props.disabled).toBe(false); + expect(findElements(tree, Alert)).toHaveLength(0); }); it('blocks another solve and offers return/discard actions outside the original room', () => { const props = makeProps(); - const wrapper = shallow(
); - const actions = shallow(
{wrapper.find(Alert).prop('action')}
); + const alert = findElements(createMain(props).render(), Alert)[0]; + const buttons = findElements(alert.props.action, Button); - expect(wrapper.find(Timer).prop('disabled')).toBe(true); - expect(wrapper.find(Alert).text()).toContain('Your saved time belongs to room original-room.'); - expect(actions.find(Button).map((button) => button.text())).toEqual([ + expect(findElements(createMain(props).render(), Timer)[0].props.disabled).toBe(true); + expect(alert.props.children).toContain('Your saved time belongs to room original-room.'); + expect(buttons.map((button) => button.props.children)).toEqual([ 'Return to room', 'Discard saved result', ]); - actions.find(Button).at(1).simulate('click'); + buttons[1].props.onClick(); expect(props.dispatch).toHaveBeenCalledWith({ type: DISCARD_PENDING_RESULT, submissionId: 'submission-one', @@ -148,10 +163,7 @@ describe('room pending result UX', () => { }); it('also blocks a solve when the device outbox belongs to another account', () => { - const anotherUsersResult = { - ...pendingResult, - userId: 99, - }; + const anotherUsersResult = { ...pendingResult, userId: 99 }; const props = makeProps({ room: { ...makeProps().room, @@ -162,13 +174,12 @@ describe('room pending result UX', () => { }, }, }); - const wrapper = shallow(
); - const actions = shallow(
{wrapper.find(Alert).prop('action')}
); + const alert = findElements(createMain(props).render(), Alert)[0]; - expect(wrapper.find(Timer).prop('disabled')).toBe(true); - expect(wrapper.find(Alert).text()).toContain('another account'); - expect(actions.find(Button)).toHaveLength(1); - expect(actions.find(Button).text()).toBe('Discard saved result'); + expect(findElements(createMain(props).render(), Timer)[0].props.disabled).toBe(true); + expect(alert.props.children).toContain('another account'); + expect(findElements(alert.props.action, Button)).toHaveLength(1); + expect(findElements(alert.props.action, Button)[0].props.children).toBe('Discard saved result'); }); it('keeps an attempted result without offering discard', () => { @@ -182,11 +193,11 @@ describe('room pending result UX', () => { }, }, }); - const wrapper = shallow(
); - const actions = shallow(
{wrapper.find(Alert).prop('action')}
); + const alert = findElements(createMain(props).render(), Alert)[0]; + const buttons = findElements(alert.props.action, Button); - expect(actions.find(Button)).toHaveLength(1); - expect(actions.find(Button).text()).toBe('Return to room'); - expect(wrapper.find(Alert).text()).toContain('finish submitting'); + expect(buttons).toHaveLength(1); + expect(buttons[0].props.children).toBe('Return to room'); + expect(alert.props.children).toContain('finish submitting'); }); }); diff --git a/client/src/components/Room/Common/ManageUsersDialog.jsx b/client/src/components/Room/Common/ManageUsersDialog.jsx index 0434a32..994ef7b 100644 --- a/client/src/components/Room/Common/ManageUsersDialog.jsx +++ b/client/src/components/Room/Common/ManageUsersDialog.jsx @@ -2,20 +2,20 @@ import React from 'react'; import clsx from 'clsx'; import { connect, useDispatch } from 'react-redux'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import Checkbox from '@material-ui/core/Checkbox'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemText from '@material-ui/core/ListItemText'; -import ListSubheader from '@material-ui/core/ListSubheader'; -import ListItemAvatar from '@material-ui/core/ListItemAvatar'; -import Avatar from '@material-ui/core/Avatar'; +import { makeStyles } from '@mui/styles'; +import Button from '@mui/material/Button'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import ListSubheader from '@mui/material/ListSubheader'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import Avatar from '@mui/material/Avatar'; import { kickUser, updateUser, updateBanned } from '../../../store/room/actions'; import { getUsersInRoom, diff --git a/client/src/components/Room/Common/Panel.jsx b/client/src/components/Room/Common/Panel.jsx index 0043d9a..4953262 100644 --- a/client/src/components/Room/Common/Panel.jsx +++ b/client/src/components/Room/Common/Panel.jsx @@ -1,10 +1,10 @@ import React from 'react'; import clsx from 'clsx'; import PropTypes from 'prop-types'; -import Paper from '@material-ui/core/Paper'; -import Toolbar from '@material-ui/core/Toolbar'; -import Divider from '@material-ui/core/Divider'; -import { makeStyles } from '@material-ui/core/styles'; +import Paper from '@mui/material/Paper'; +import Toolbar from '@mui/material/Toolbar'; +import Divider from '@mui/material/Divider'; +import { makeStyles } from '@mui/styles'; const useStyles = makeStyles(() => ({ root: { diff --git a/client/src/components/Room/Common/StatsDialog.jsx b/client/src/components/Room/Common/StatsDialog.jsx index 7ec72cc..204f4cb 100644 --- a/client/src/components/Room/Common/StatsDialog.jsx +++ b/client/src/components/Room/Common/StatsDialog.jsx @@ -1,13 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import Typography from '@material-ui/core/Typography'; -import Paper from '@material-ui/core/Paper'; +import { makeStyles } from '@mui/styles'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import Typography from '@mui/material/Typography'; +import Paper from '@mui/material/Paper'; import { formatTime } from '../../../lib/utils'; const useStyles = makeStyles(() => ({ diff --git a/client/src/components/Room/Common/TableStatusCell.jsx b/client/src/components/Room/Common/TableStatusCell.jsx index 23bf4f4..c26c85f 100644 --- a/client/src/components/Room/Common/TableStatusCell.jsx +++ b/client/src/components/Room/Common/TableStatusCell.jsx @@ -1,9 +1,9 @@ import React from 'react'; import clsx from 'clsx'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; -import TableCell from '@material-ui/core/TableCell'; +import { makeStyles } from '@mui/styles'; +import Typography from '@mui/material/Typography'; +import TableCell from '@mui/material/TableCell'; const useStyles = makeStyles(() => ({ root: { diff --git a/client/src/components/Room/Common/TableTimeCell.jsx b/client/src/components/Room/Common/TableTimeCell.jsx index 03939f3..f9cdabb 100644 --- a/client/src/components/Room/Common/TableTimeCell.jsx +++ b/client/src/components/Room/Common/TableTimeCell.jsx @@ -1,10 +1,10 @@ import React from 'react'; import clsx from 'clsx'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; -import Typography from '@material-ui/core/Typography'; -import TableCell from '@material-ui/core/TableCell'; +import { makeStyles } from '@mui/styles'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import TableCell from '@mui/material/TableCell'; import { formatTime } from '../../../lib/utils'; import { useEditDialog } from './EditDialogProvider'; diff --git a/client/src/components/Room/Common/TimesTable.jsx b/client/src/components/Room/Common/TimesTable.jsx index 54d8cd2..e6e1451 100644 --- a/client/src/components/Room/Common/TimesTable.jsx +++ b/client/src/components/Room/Common/TimesTable.jsx @@ -1,14 +1,14 @@ import React, { createRef } from 'react'; import clsx from 'clsx'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; -import TableContainer from '@material-ui/core/TableContainer'; -import Table from '@material-ui/core/Table'; -import TableHead from '@material-ui/core/TableHead'; -import TableBody from '@material-ui/core/TableBody'; -import TableRow from '@material-ui/core/TableRow'; -import TableCell from '@material-ui/core/TableCell'; +import { makeStyles } from '@mui/styles'; +import Typography from '@mui/material/Typography'; +import TableContainer from '@mui/material/TableContainer'; +import Table from '@mui/material/Table'; +import TableHead from '@mui/material/TableHead'; +import TableBody from '@mui/material/TableBody'; +import TableRow from '@mui/material/TableRow'; +import TableCell from '@mui/material/TableCell'; import { useStatsDialog } from './StatsDialogProvider'; import TableCellButton from '../../common/TableCellButton'; import TableStatusCell from './TableStatusCell'; diff --git a/client/src/components/Room/Common/UserSelectorDialog.jsx b/client/src/components/Room/Common/UserSelectorDialog.jsx index 4d15637..22eb97e 100644 --- a/client/src/components/Room/Common/UserSelectorDialog.jsx +++ b/client/src/components/Room/Common/UserSelectorDialog.jsx @@ -1,18 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; -import Checkbox from '@material-ui/core/Checkbox'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemText from '@material-ui/core/ListItemText'; -import ListItemAvatar from '@material-ui/core/ListItemAvatar'; -import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; -import Avatar from '@material-ui/core/Avatar'; +import { makeStyles } from '@mui/styles'; +import Button from '@mui/material/Button'; +import Checkbox from '@mui/material/Checkbox'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; +import Avatar from '@mui/material/Avatar'; const useStyles = makeStyles(() => ({})); diff --git a/client/src/components/Room/Common/UserStats.jsx b/client/src/components/Room/Common/UserStats.jsx index 08e0c13..4d3379a 100644 --- a/client/src/components/Room/Common/UserStats.jsx +++ b/client/src/components/Room/Common/UserStats.jsx @@ -1,12 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import Paper from '@material-ui/core/Paper'; -import Table from '@material-ui/core/Table'; -import TableHead from '@material-ui/core/TableHead'; -import TableBody from '@material-ui/core/TableBody'; -import TableRow from '@material-ui/core/TableRow'; -import TableCell from '@material-ui/core/TableCell'; +import { makeStyles } from '@mui/styles'; +import Paper from '@mui/material/Paper'; +import Table from '@mui/material/Table'; +import TableHead from '@mui/material/TableHead'; +import TableBody from '@mui/material/TableBody'; +import TableRow from '@mui/material/TableRow'; +import TableCell from '@mui/material/TableCell'; import { formatTime } from '../../../lib/utils'; const useStyles = makeStyles((theme) => ({ diff --git a/client/src/components/Room/Common/UserToolbar.jsx b/client/src/components/Room/Common/UserToolbar.jsx index 1c34885..ffae149 100644 --- a/client/src/components/Room/Common/UserToolbar.jsx +++ b/client/src/components/Room/Common/UserToolbar.jsx @@ -1,21 +1,21 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles } from '@mui/styles'; import { connect } from 'react-redux'; -import Toolbar from '@material-ui/core/Toolbar'; -import FormGroup from '@material-ui/core/FormGroup'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import SettingsIcon from '@material-ui/icons/Settings'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import FormControl from '@material-ui/core/FormControl'; -import InputLabel from '@material-ui/core/InputLabel'; -import Checkbox from '@material-ui/core/Checkbox'; -import Select from '@material-ui/core/Select'; -import MenuItem from '@material-ui/core/MenuItem'; +import Toolbar from '@mui/material/Toolbar'; +import FormGroup from '@mui/material/FormGroup'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import SettingsIcon from '@mui/icons-material/Settings'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import Checkbox from '@mui/material/Checkbox'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; import { lcFetch } from '../../../lib/fetch'; import { updateCompeting } from '../../../store/room/actions'; import { updateProfile } from '../../../store/user/actions'; diff --git a/client/src/components/Room/GrandPrix/GrandPrixMain.jsx b/client/src/components/Room/GrandPrix/GrandPrixMain.jsx index d88567e..f80298d 100644 --- a/client/src/components/Room/GrandPrix/GrandPrixMain.jsx +++ b/client/src/components/Room/GrandPrix/GrandPrixMain.jsx @@ -1,14 +1,15 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { withStyles, makeStyles, useTheme } from '@material-ui/core/styles'; +import { useTheme } from '@mui/material/styles'; +import { withStyles, makeStyles } from '@mui/styles'; import { connect, useDispatch } from 'react-redux'; -import Grid from '@material-ui/core/Grid'; -import Box from '@material-ui/core/Box'; -import Paper from '@material-ui/core/Paper'; -import Divider from '@material-ui/core/Divider'; -import Typography from '@material-ui/core/Typography'; -import ClickAwayListener from '@material-ui/core/ClickAwayListener'; -import Button from '@material-ui/core/Button'; +import Grid from '@mui/material/Grid'; +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import Divider from '@mui/material/Divider'; +import Typography from '@mui/material/Typography'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import Button from '@mui/material/Button'; import { formatISO9075 } from 'date-fns'; import calcStats from '../../../lib/stats'; import { diff --git a/client/src/components/Room/GrandPrix/Leaderboard.jsx b/client/src/components/Room/GrandPrix/Leaderboard.jsx index 68afe90..42ecf00 100644 --- a/client/src/components/Room/GrandPrix/Leaderboard.jsx +++ b/client/src/components/Room/GrandPrix/Leaderboard.jsx @@ -1,16 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect, useDispatch } from 'react-redux'; -import { makeStyles } from '@material-ui/core/styles'; -import Toolbar from '@material-ui/core/Toolbar'; -import Typography from '@material-ui/core/Typography'; -import Paper from '@material-ui/core/Paper'; -import TableContainer from '@material-ui/core/TableContainer'; -import Table from '@material-ui/core/Table'; -import TableHead from '@material-ui/core/TableHead'; -import TableBody from '@material-ui/core/TableBody'; -import TableRow from '@material-ui/core/TableRow'; -import TableCell from '@material-ui/core/TableCell'; +import { makeStyles } from '@mui/styles'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import Paper from '@mui/material/Paper'; +import TableContainer from '@mui/material/TableContainer'; +import Table from '@mui/material/Table'; +import TableHead from '@mui/material/TableHead'; +import TableBody from '@mui/material/TableBody'; +import TableRow from '@mui/material/TableRow'; +import TableCell from '@mui/material/TableCell'; import TableStatusCell from '../Common/TableStatusCell'; import TableTimeCell from '../Common/TableTimeCell'; import { StatsDialogProvider } from '../Common/StatsDialogProvider'; diff --git a/client/src/components/Room/GrandPrix/RegisterPanel.jsx b/client/src/components/Room/GrandPrix/RegisterPanel.jsx index f426b26..f94ec7e 100644 --- a/client/src/components/Room/GrandPrix/RegisterPanel.jsx +++ b/client/src/components/Room/GrandPrix/RegisterPanel.jsx @@ -2,10 +2,10 @@ import React, { useState } from 'react'; import { connect, useDispatch } from 'react-redux'; import PropTypes from 'prop-types'; import { formatDistanceToNow, formatRFC7231 } from 'date-fns'; -import { makeStyles } from '@material-ui/core/styles'; -import Box from '@material-ui/core/Box'; -import Typography from '@material-ui/core/Typography'; -import Button from '@material-ui/core/Button'; +import { makeStyles } from '@mui/styles'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; import { updateRegistration } from '../../../store/room/actions'; import { getRegisteredUsers } from '../../../store/room/selectors'; diff --git a/client/src/components/Room/GrandPrix/index.jsx b/client/src/components/Room/GrandPrix/index.jsx index 7a1cf3e..cbf779c 100644 --- a/client/src/components/Room/GrandPrix/index.jsx +++ b/client/src/components/Room/GrandPrix/index.jsx @@ -1,21 +1,21 @@ import React, { useState } from 'react'; import clsx from 'clsx'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles } from '@mui/styles'; import { connect } from 'react-redux'; -import Grid from '@material-ui/core/Grid'; -import AppBar from '@material-ui/core/AppBar'; -import Toolbar from '@material-ui/core/Toolbar'; -import Typography from '@material-ui/core/Typography'; -import Paper from '@material-ui/core/Paper'; -import BottomNavigation from '@material-ui/core/BottomNavigation'; -import BottomNavigationAction from '@material-ui/core/BottomNavigationAction'; -import Divider from '@material-ui/core/Divider'; -import IconButton from '@material-ui/core/IconButton'; -import ChatIcon from '@material-ui/icons/Chat'; -import TimerIcon from '@material-ui/icons/Timer'; -import ArrowForwardIcon from '@material-ui/icons/ArrowForward'; -import ArrowBackIcon from '@material-ui/icons/ArrowBack'; +import Grid from '@mui/material/Grid'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import Paper from '@mui/material/Paper'; +import BottomNavigation from '@mui/material/BottomNavigation'; +import BottomNavigationAction from '@mui/material/BottomNavigationAction'; +import Divider from '@mui/material/Divider'; +import IconButton from '@mui/material/IconButton'; +import ChatIcon from '@mui/icons-material/Chat'; +import TimerIcon from '@mui/icons-material/Timer'; +import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import Main from './GrandPrixMain'; import AdminToolbar from '../Common/AdminToolbar'; import UserToolbar from '../Common/UserToolbar'; diff --git a/client/src/components/Room/Normal/index.jsx b/client/src/components/Room/Normal/index.jsx index 1ab53d4..465bc61 100644 --- a/client/src/components/Room/Normal/index.jsx +++ b/client/src/components/Room/Normal/index.jsx @@ -1,15 +1,15 @@ import React, { useState } from 'react'; import clsx from 'clsx'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles } from '@mui/styles'; import { connect } from 'react-redux'; -import Grid from '@material-ui/core/Grid'; -import Paper from '@material-ui/core/Paper'; -import BottomNavigation from '@material-ui/core/BottomNavigation'; -import BottomNavigationAction from '@material-ui/core/BottomNavigationAction'; -import Divider from '@material-ui/core/Divider'; -import ChatIcon from '@material-ui/icons/Chat'; -import TimerIcon from '@material-ui/icons/Timer'; +import Grid from '@mui/material/Grid'; +import Paper from '@mui/material/Paper'; +import BottomNavigation from '@mui/material/BottomNavigation'; +import BottomNavigationAction from '@mui/material/BottomNavigationAction'; +import Divider from '@mui/material/Divider'; +import ChatIcon from '@mui/icons-material/Chat'; +import TimerIcon from '@mui/icons-material/Timer'; import Main from '../Common/Main'; import Chat from '../Panels/Chat'; import AdminToolbar from '../Common/AdminToolbar'; diff --git a/client/src/components/Room/Panels/Chat.jsx b/client/src/components/Room/Panels/Chat.jsx index b02a11a..d29e5a2 100644 --- a/client/src/components/Room/Panels/Chat.jsx +++ b/client/src/components/Room/Panels/Chat.jsx @@ -2,25 +2,25 @@ import React, { useState, useRef, useEffect } from 'react'; import clsx from 'clsx'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { makeStyles } from '@material-ui/core/styles'; -import Paper from '@material-ui/core/Paper'; -import Toolbar from '@material-ui/core/Toolbar'; -import FormControl from '@material-ui/core/FormControl'; -import Input from '@material-ui/core/Input'; -import Alert from '@material-ui/lab/Alert'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemText from '@material-ui/core/ListItemText'; -import ListItemAvatar from '@material-ui/core/ListItemAvatar'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import Avatar from '@material-ui/core/Avatar'; -import Typography from '@material-ui/core/Typography'; -import IconButton from '@material-ui/core/IconButton'; -import Icon from '@material-ui/core/Icon'; -import NotificationImportantIcon from '@material-ui/icons/NotificationImportant'; -import PersonIcon from '@material-ui/icons/Person'; -import ArrowForwardIcon from '@material-ui/icons/ArrowForward'; -import ArrowBackIcon from '@material-ui/icons/ArrowBack'; +import { makeStyles } from '@mui/styles'; +import Paper from '@mui/material/Paper'; +import Toolbar from '@mui/material/Toolbar'; +import FormControl from '@mui/material/FormControl'; +import Input from '@mui/material/Input'; +import Alert from '@mui/material/Alert'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import Avatar from '@mui/material/Avatar'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import Icon from '@mui/material/Icon'; +import NotificationImportantIcon from '@mui/icons-material/NotificationImportant'; +import PersonIcon from '@mui/icons-material/Person'; +import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { sendChat } from '../../../store/chat/actions'; import Panel from '../Common/Panel'; diff --git a/client/src/components/Room/index.jsx b/client/src/components/Room/index.jsx index 8b449a4..d9910ae 100644 --- a/client/src/components/Room/index.jsx +++ b/client/src/components/Room/index.jsx @@ -1,11 +1,11 @@ import React, { useEffect } from 'react'; import { useLocation, useParams } from 'react-router-dom'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles } from '@mui/styles'; import { connect, useDispatch } from 'react-redux'; -import Paper from '@material-ui/core/Paper'; -import Backdrop from '@material-ui/core/Backdrop'; -import CircularProgress from '@material-ui/core/CircularProgress'; +import Paper from '@mui/material/Paper'; +import Backdrop from '@mui/material/Backdrop'; +import CircularProgress from '@mui/material/CircularProgress'; import qs from 'qs'; import Login from './Common/Login'; import { diff --git a/client/src/components/RoomConfigureDialog.jsx b/client/src/components/RoomConfigureDialog.jsx index 379f7b4..0535d0a 100644 --- a/client/src/components/RoomConfigureDialog.jsx +++ b/client/src/components/RoomConfigureDialog.jsx @@ -2,26 +2,26 @@ import React from 'react'; import clsx from 'clsx'; import PropTypes from 'prop-types'; import { format } from 'date-fns'; -import { makeStyles } from '@material-ui/core/styles'; -import Divider from '@material-ui/core/Divider'; -import Typography from '@material-ui/core/Typography'; -import Button from '@material-ui/core/Button'; -import TextField from '@material-ui/core/TextField'; -import Switch from '@material-ui/core/Switch'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import FormControl from '@material-ui/core/FormControl'; -import FormHelperText from '@material-ui/core/FormHelperText'; -import InputLabel from '@material-ui/core/InputLabel'; -import Select from '@material-ui/core/Select'; -import MenuItem from '@material-ui/core/MenuItem'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import Accordion from '@material-ui/core/Accordion'; -import AccordionDetails from '@material-ui/core/AccordionDetails'; -import AccordionSummary from '@material-ui/core/AccordionSummary'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { makeStyles } from '@mui/styles'; +import Divider from '@mui/material/Divider'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import Switch from '@mui/material/Switch'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import Accordion from '@mui/material/Accordion'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; const useStyles = makeStyles((theme) => ({ fab: { @@ -117,6 +117,7 @@ function RoomConfigureDialog({ autoComplete="off" autoFocus fullWidth + margin="dense" /> render( + + + , +); + describe('RoomConfigureDialog passwords', () => { it('allows an existing private room to be edited without replacing its password', () => { const onSave = jest.fn(); - const wrapper = shallow( - , - ); + renderDialog({ + room: privateRoom, + open: true, + onSave, + onCancel: jest.fn(), + }); - const save = wrapper.find(Button).filterWhere((button) => button.text() === 'Save'); - expect(save.prop('disabled')).toBe(false); - save.simulate('click'); + fireEvent.click(screen.getByRole('button', { name: 'Save' })); expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ private: true, @@ -39,18 +39,15 @@ describe('RoomConfigureDialog passwords', () => { it('sends an explicitly changed password', () => { const onSave = jest.fn(); - const wrapper = shallow( - , - ); + renderDialog({ + room: privateRoom, + open: true, + onSave, + onCancel: jest.fn(), + }); - wrapper.find(TextField).findWhere((field) => field.prop('id') === 'password') - .simulate('change', { target: { value: 'new-password' } }); - wrapper.find(Button).filterWhere((button) => button.text() === 'Save').simulate('click'); + fireEvent.change(screen.getByLabelText('New Password'), { target: { value: 'new-password' } }); + fireEvent.click(screen.getByRole('button', { name: 'Save' })); expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ private: true, @@ -59,35 +56,27 @@ describe('RoomConfigureDialog passwords', () => { }); it('requires a password when making an existing public room private', () => { - const wrapper = shallow( - , - ); + renderDialog({ + room: { ...privateRoom, private: false }, + open: true, + onSave: jest.fn(), + onCancel: jest.fn(), + }); - wrapper.find(FormControlLabel).first().prop('control').props.onChange(); - wrapper.update(); + fireEvent.click(screen.getByRole('checkbox', { name: 'Private Room?' })); - const save = wrapper.find(Button).filterWhere((button) => button.text() === 'Save'); - expect(save.prop('disabled')).toBe(true); + expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled(); }); it('requires a password when creating a private room', () => { - const wrapper = shallow( - , - ); + renderDialog({ + open: true, + onSave: jest.fn(), + onCancel: jest.fn(), + }); - wrapper.find(FormControlLabel).first().prop('control').props.onChange(); - wrapper.update(); + fireEvent.click(screen.getByRole('checkbox', { name: 'Private Room?' })); - const create = wrapper.find(Button).filterWhere((button) => button.text() === 'Create'); - expect(create.prop('disabled')).toBe(true); + expect(screen.getByRole('button', { name: 'Create' })).toBeDisabled(); }); }); diff --git a/client/src/components/RoomListItem.jsx b/client/src/components/RoomListItem.jsx index 0dfcbf8..eeddca6 100644 --- a/client/src/components/RoomListItem.jsx +++ b/client/src/components/RoomListItem.jsx @@ -3,18 +3,18 @@ import { useHistory } from 'react-router-dom'; import { connect, useDispatch } from 'react-redux'; import PropTypes from 'prop-types'; import { formatDistanceToNow } from 'date-fns'; -import Tooltip from '@material-ui/core/Tooltip'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import ListItemText from '@material-ui/core/ListItemText'; -import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; -import Typography from '@material-ui/core/Typography'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import IconButton from '@material-ui/core/IconButton'; -import PublicIcon from '@material-ui/icons/Public'; -import PrivateIcon from '@material-ui/icons/Lock'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; +import Tooltip from '@mui/material/Tooltip'; +import ListItem from '@mui/material/ListItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; +import Typography from '@mui/material/Typography'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import IconButton from '@mui/material/IconButton'; +import PublicIcon from '@mui/icons-material/Public'; +import PrivateIcon from '@mui/icons-material/Lock'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; import { useConfirm } from 'material-ui-confirm'; import { lcFetch } from '../lib/fetch'; // import ListItem from './ListItemLink'; diff --git a/client/src/components/Timer/ManualTimer.jsx b/client/src/components/Timer/ManualTimer.jsx index 4b69a6d..087a650 100644 --- a/client/src/components/Timer/ManualTimer.jsx +++ b/client/src/components/Timer/ManualTimer.jsx @@ -1,19 +1,19 @@ import React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import { withStyles } from '@material-ui/core/styles'; -import Box from '@material-ui/core/Box'; -import Typography from '@material-ui/core/Typography'; -import Button from '@material-ui/core/Button'; -import Input from '@material-ui/core/Input'; -import Checkbox from '@material-ui/core/Checkbox'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; +import { withStyles } from '@mui/styles'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import Input from '@mui/material/Input'; +import Checkbox from '@mui/material/Checkbox'; +import FormControlLabel from '@mui/material/FormControlLabel'; import { formatTime, parseTime, setInterval, clearInterval, now, } from '../../lib/utils'; import initialStyles from './styles'; -const useStyles = withStyles((theme) => ({ +const withComponentStyles = withStyles((theme) => ({ ...initialStyles(theme), input: { maxWidth: '300px', @@ -559,4 +559,4 @@ ManualTimer.defaultProps = { onPriming: () => {}, }; -export default useStyles(ManualTimer); +export default withComponentStyles(ManualTimer); diff --git a/client/src/components/Timer/ManualTimer.test.jsx b/client/src/components/Timer/ManualTimer.test.jsx index a149cb2..11a0b68 100644 --- a/client/src/components/Timer/ManualTimer.test.jsx +++ b/client/src/components/Timer/ManualTimer.test.jsx @@ -1,5 +1,3 @@ -import React from 'react'; -import { shallow } from 'enzyme'; import { ManualTimer } from './ManualTimer'; const makeProps = () => ({ @@ -15,24 +13,32 @@ const makeProps = () => ({ useInspection: false, }); +const createTimer = (props) => { + const timer = new ManualTimer(props); + timer.setState = (update) => { + timer.state = { ...timer.state, ...update }; + }; + return timer; +}; + describe('manual timer submission', () => { it.each(['', 'not-a-time'])('keeps invalid input for correction: %p', (timeInput) => { const props = makeProps(); - const wrapper = shallow(); - wrapper.setState({ timeInput }); + const timer = createTimer(props); + timer.state.timeInput = timeInput; - wrapper.instance().submitTime({ preventDefault: jest.fn() }); + timer.submitTime({ preventDefault: jest.fn() }); expect(props.onSubmitTime).not.toHaveBeenCalled(); - expect(wrapper.state('timeInput')).toBe(timeInput); + expect(timer.state.timeInput).toBe(timeInput); }); it('submits and resets a valid time', () => { const props = makeProps(); - const wrapper = shallow(); - wrapper.setState({ timeInput: '5.00' }); + const timer = createTimer(props); + timer.state.timeInput = '5.00'; - wrapper.instance().submitTime({ preventDefault: jest.fn() }); + timer.submitTime({ preventDefault: jest.fn() }); expect(props.onSubmitTime).toHaveBeenCalledWith({ time: 5000, @@ -42,6 +48,6 @@ describe('manual timer submission', () => { inspection: false, }, }); - expect(wrapper.state('timeInput')).toBe(''); + expect(timer.state.timeInput).toBe(''); }); }); diff --git a/client/src/components/Timer/SpacebarTimer.jsx b/client/src/components/Timer/SpacebarTimer.jsx index d291dec..cf6f2ed 100644 --- a/client/src/components/Timer/SpacebarTimer.jsx +++ b/client/src/components/Timer/SpacebarTimer.jsx @@ -1,19 +1,19 @@ import React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import { withStyles } from '@material-ui/core/styles'; -import Box from '@material-ui/core/Box'; -import Typography from '@material-ui/core/Typography'; -import Button from '@material-ui/core/Button'; -import Checkbox from '@material-ui/core/Checkbox'; -import FormGroup from '@material-ui/core/FormGroup'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; +import { withStyles } from '@mui/styles'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import Checkbox from '@mui/material/Checkbox'; +import FormGroup from '@mui/material/FormGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; import { formatTime, setInterval, clearInterval, now, } from '../../lib/utils'; import styles from './styles'; -const useStyles = withStyles(styles); +const withComponentStyles = withStyles(styles); /* Complicated beast. @@ -475,4 +475,4 @@ Timer.defaultProps = { onPriming: () => {}, }; -export default useStyles(Timer); +export default withComponentStyles(Timer); diff --git a/client/src/components/Timer/StackmatTimer.jsx b/client/src/components/Timer/StackmatTimer.jsx index e1b2045..a68e5cd 100644 --- a/client/src/components/Timer/StackmatTimer.jsx +++ b/client/src/components/Timer/StackmatTimer.jsx @@ -3,19 +3,19 @@ import StackmatSignalProcessor from 'stackmat-signal-processor'; import React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import { withStyles } from '@material-ui/core/styles'; -import Box from '@material-ui/core/Box'; -import Typography from '@material-ui/core/Typography'; -import Button from '@material-ui/core/Button'; -import Checkbox from '@material-ui/core/Checkbox'; -import FormGroup from '@material-ui/core/FormGroup'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; +import { withStyles } from '@mui/styles'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import Checkbox from '@mui/material/Checkbox'; +import FormGroup from '@mui/material/FormGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; import { formatTime, setInterval, clearInterval, now, } from '../../lib/utils'; import styles from './styles'; -const useStyles = withStyles(styles); +const withComponentStyles = withStyles(styles); const STATUS = { RESTING: 'RESTING', @@ -431,4 +431,4 @@ Timer.defaultProps = { onPriming: () => {}, }; -export default useStyles(Timer); +export default withComponentStyles(Timer); diff --git a/client/src/components/Timer/index.jsx b/client/src/components/Timer/index.jsx index d796505..a0be36e 100644 --- a/client/src/components/Timer/index.jsx +++ b/client/src/components/Timer/index.jsx @@ -1,4 +1,4 @@ -/* eslint-disable react/jsx-props-no-spreading */ + import React from 'react'; import PropTypes from 'prop-types'; import SpacebarTimer from './SpacebarTimer'; diff --git a/client/src/components/User.jsx b/client/src/components/User.jsx index 847ce45..5e07e90 100644 --- a/client/src/components/User.jsx +++ b/client/src/components/User.jsx @@ -1,9 +1,9 @@ import React from 'react'; import clsx from 'clsx'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import Popover from '@material-ui/core/Popover'; -import Typography from '@material-ui/core/Typography'; +import { makeStyles } from '@mui/styles'; +import Popover from '@mui/material/Popover'; +import Typography from '@mui/material/Typography'; import { Link } from 'react-router-dom'; const useStyles = makeStyles((theme) => ({ diff --git a/client/src/components/User.test.jsx b/client/src/components/User.test.jsx index 84217a1..cb99e4e 100644 --- a/client/src/components/User.test.jsx +++ b/client/src/components/User.test.jsx @@ -1,23 +1,31 @@ import React from 'react'; -import { shallow } from 'enzyme'; -import Typography from '@material-ui/core/Typography'; +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { ThemeProvider } from '../theme'; import User from './User'; describe('User', () => { const baseUser = { displayName: 'Cuber', id: 42 }; + const renderUser = (user) => render( + + + + + , + ); + it('does not turn an internal user ID into a public profile link', () => { - const wrapper = shallow(); - const name = wrapper.find(Typography).first(); + renderUser(baseUser); + const name = screen.getByText('Cuber'); - expect(name.prop('component')).toBe('span'); - expect(name.prop('to')).toBeUndefined(); + expect(name.tagName).toBe('SPAN'); + expect(name).not.toHaveAttribute('href'); }); it('links only through a server-provided public profile key', () => { - const wrapper = shallow(); - const name = wrapper.find(Typography).first(); + renderUser({ ...baseUser, profileKey: 'cuber' }); - expect(name.prop('to')).toBe('/users/cuber'); + expect(screen.getByText('Cuber')).toHaveAttribute('href', '/users/cuber'); }); }); diff --git a/client/src/components/WCARedirect.jsx b/client/src/components/WCARedirect.jsx index 52a3f06..17cad31 100644 --- a/client/src/components/WCARedirect.jsx +++ b/client/src/components/WCARedirect.jsx @@ -3,13 +3,13 @@ import PropTypes from 'prop-types'; import { connect, useDispatch } from 'react-redux'; import qs from 'qs'; import { Redirect, useLocation } from 'react-router-dom'; -import { makeStyles } from '@material-ui/core/styles'; -import Box from '@material-ui/core/Box'; -import Paper from '@material-ui/core/Paper'; -import Typography from '@material-ui/core/Typography'; -import LinearProgress from '@material-ui/core/LinearProgress'; -import Alert from '@material-ui/lab/Alert'; -import AlertTitle from '@material-ui/lab/AlertTitle'; +import { makeStyles } from '@mui/styles'; +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import LinearProgress from '@mui/material/LinearProgress'; +import Alert from '@mui/material/Alert'; +import AlertTitle from '@mui/material/AlertTitle'; import { lcFetch } from '../lib/fetch'; import { userChanged } from '../store/user/actions'; diff --git a/client/src/components/common/EditableTextField.jsx b/client/src/components/common/EditableTextField.jsx index 8302b26..93a83e8 100644 --- a/client/src/components/common/EditableTextField.jsx +++ b/client/src/components/common/EditableTextField.jsx @@ -1,14 +1,14 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import Paper from '@material-ui/core/Paper'; -import Input from '@material-ui/core/Input'; -import InputLabel from '@material-ui/core/InputLabel'; -import Button from '@material-ui/core/Button'; -import ButtonGroup from '@material-ui/core/ButtonGroup'; -import FormControl from '@material-ui/core/FormControl'; -import FormHelperText from '@material-ui/core/FormHelperText'; -import ClickAwayListener from '@material-ui/core/ClickAwayListener'; +import { makeStyles } from '@mui/styles'; +import Paper from '@mui/material/Paper'; +import Input from '@mui/material/Input'; +import InputLabel from '@mui/material/InputLabel'; +import Button from '@mui/material/Button'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; const useStyles = makeStyles((theme) => ({ root: { diff --git a/client/src/components/common/EventListItem.jsx b/client/src/components/common/EventListItem.jsx index b953217..c990023 100644 --- a/client/src/components/common/EventListItem.jsx +++ b/client/src/components/common/EventListItem.jsx @@ -3,23 +3,24 @@ import { useHistory } from 'react-router-dom'; import { connect, useDispatch } from 'react-redux'; import PropTypes from 'prop-types'; import { formatDistanceToNow } from 'date-fns'; -import Tooltip from '@material-ui/core/Tooltip'; -import { makeStyles, useTheme } from '@material-ui/core/styles'; -import Card from '@material-ui/core/Card'; -import CardHeader from '@material-ui/core/CardHeader'; -import CardContent from '@material-ui/core/CardContent'; -import CardActions from '@material-ui/core/CardActions'; -import CardMedia from '@material-ui/core/CardMedia'; -import Alert from '@material-ui/lab/Alert'; -import Typography from '@material-ui/core/Typography'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import IconButton from '@material-ui/core/IconButton'; -import Button from '@material-ui/core/Button'; -import PublicIcon from '@material-ui/icons/Public'; -import PrivateIcon from '@material-ui/icons/Lock'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; -import ShareIcon from '@material-ui/icons/Share'; +import Tooltip from '@mui/material/Tooltip'; +import { useTheme } from '@mui/material/styles'; +import { makeStyles } from '@mui/styles'; +import Card from '@mui/material/Card'; +import CardHeader from '@mui/material/CardHeader'; +import CardContent from '@mui/material/CardContent'; +import CardActions from '@mui/material/CardActions'; +import CardMedia from '@mui/material/CardMedia'; +import Alert from '@mui/material/Alert'; +import Typography from '@mui/material/Typography'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import IconButton from '@mui/material/IconButton'; +import Button from '@mui/material/Button'; +import PublicIcon from '@mui/icons-material/Public'; +import PrivateIcon from '@mui/icons-material/Lock'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import ShareIcon from '@mui/icons-material/Share'; import { useConfirm } from 'material-ui-confirm'; import { TwitchEmbed } from 'react-twitch-embed'; import { lcFetch } from '../../lib/fetch'; @@ -167,7 +168,7 @@ function RoomListItem({ id={`channel-${room.twitchChannel}-${room._id}`} muted withChat={false} - theme={theme.palette.type} + theme={theme.palette.mode} width="100%" /> diff --git a/client/src/components/common/HelpPopover.jsx b/client/src/components/common/HelpPopover.jsx index ba58ece..b0cd17f 100644 --- a/client/src/components/common/HelpPopover.jsx +++ b/client/src/components/common/HelpPopover.jsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; -import Popover from '@material-ui/core/Popover'; -import Typography from '@material-ui/core/Typography'; -import IconButton from '@material-ui/core/IconButton'; -import HelpIcon from '@material-ui/icons/Help'; +import Popover from '@mui/material/Popover'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import HelpIcon from '@mui/icons-material/Help'; -export default () => { +const HelpPopover = () => { const [helpAnchor, setHelpAnchor] = useState(null); return ( @@ -39,3 +39,5 @@ export default () => {
); }; + +export default HelpPopover; diff --git a/client/src/components/common/ListItemLink.jsx b/client/src/components/common/ListItemLink.jsx index 26318db..74e3db7 100644 --- a/client/src/components/common/ListItemLink.jsx +++ b/client/src/components/common/ListItemLink.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; -import ListItem from '@material-ui/core/ListItem'; +import ListItem from '@mui/material/ListItem'; function ListItemLink({ children, to, disabled }) { return ( diff --git a/client/src/components/common/PrivateRoute.jsx b/client/src/components/common/PrivateRoute.jsx index d1564e2..e91d6b8 100644 --- a/client/src/components/common/PrivateRoute.jsx +++ b/client/src/components/common/PrivateRoute.jsx @@ -4,7 +4,7 @@ import { Route, Redirect } from 'react-router-dom'; import { connect } from 'react-redux'; const PrivateRoute = ({ - user, children, isAdminRoute, ...rest + user, isAdminRoute, ...rest }) => { const { fetching, id } = user; const loggedIn = !fetching && id; @@ -24,16 +24,11 @@ const PrivateRoute = ({ }; PrivateRoute.propTypes = { - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - ]), isAdminRoute: PropTypes.bool, user: PropTypes.shape(), }; PrivateRoute.defaultProps = { - children: [], isAdminRoute: false, user: { }, diff --git a/client/src/components/common/Profile.jsx b/client/src/components/common/Profile.jsx index dd4aa38..b389300 100644 --- a/client/src/components/common/Profile.jsx +++ b/client/src/components/common/Profile.jsx @@ -1,15 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { makeStyles } from '@material-ui/core/styles'; -import Paper from '@material-ui/core/Paper'; -import Grid from '@material-ui/core/Grid'; -import Avatar from '@material-ui/core/Avatar'; -import Typography from '@material-ui/core/Typography'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import Checkbox from '@material-ui/core/Checkbox'; -import Divider from '@material-ui/core/Divider'; -import Alert from '@material-ui/lab/Alert'; +import { makeStyles } from '@mui/styles'; +import Paper from '@mui/material/Paper'; +import Grid from '@mui/material/Grid'; +import Avatar from '@mui/material/Avatar'; +import Typography from '@mui/material/Typography'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import Divider from '@mui/material/Divider'; +import Alert from '@mui/material/Alert'; import EditableTextField from './EditableTextField'; import { lcFetch } from '../../lib/fetch'; import { updateProfile } from '../../store/user/actions'; diff --git a/client/src/components/common/Scramble.jsx b/client/src/components/common/Scramble.jsx index 5629e7e..6556cba 100644 --- a/client/src/components/common/Scramble.jsx +++ b/client/src/components/common/Scramble.jsx @@ -1,8 +1,8 @@ import React from 'react'; import clsx from 'clsx'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; +import { makeStyles } from '@mui/styles'; +import Typography from '@mui/material/Typography'; const useStyles = makeStyles((theme) => ({ root: { @@ -18,7 +18,7 @@ const useStyles = makeStyles((theme) => ({ disabled: { color: theme.palette.text.disabled, }, - // eslint-disable-next-line quote-props + minx: { lineHeight: '.90rem', fontSize: '.75rem', diff --git a/client/src/components/common/Scramble.test.jsx b/client/src/components/common/Scramble.test.jsx index 1fbad03..76e2ba2 100644 --- a/client/src/components/common/Scramble.test.jsx +++ b/client/src/components/common/Scramble.test.jsx @@ -1,15 +1,25 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import Scramble from './Scramble'; const scramble = 'test'; +const theme = createTheme(); + +const renderScramble = (props) => render( + + + , +); it('renders without crashing', () => { - shallow(); + renderScramble({ scrambles: [scramble] }); + + expect(screen.getByText(scramble)).toBeInTheDocument(); }); it('renders 3x3 text correctly', () => { - const component = shallow(); + renderScramble({ event: '333', scrambles: [scramble] }); - expect(component.text()).toEqual(scramble); + expect(screen.getByText(scramble)).toHaveTextContent(scramble); }); diff --git a/client/src/components/common/ScramblePreview.jsx b/client/src/components/common/ScramblePreview.jsx index 1841570..9359b2f 100644 --- a/client/src/components/common/ScramblePreview.jsx +++ b/client/src/components/common/ScramblePreview.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; -import Dialog from '@material-ui/core/Dialog'; -import DialogContent from '@material-ui/core/DialogContent'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; import loadTwistyPlayer from '../../lib/cubingTwisty'; import Scramble from './Scramble'; diff --git a/client/src/components/common/ScramblePreview.test.jsx b/client/src/components/common/ScramblePreview.test.jsx index 92ce1e5..d397e49 100644 --- a/client/src/components/common/ScramblePreview.test.jsx +++ b/client/src/components/common/ScramblePreview.test.jsx @@ -1,7 +1,8 @@ import React from 'react'; import { act, fireEvent, render } from '@testing-library/react'; -import Events from 'letscube-scrambles/events'; +import Events from '../../lib/events.json'; import loadTwistyPlayer from '../../lib/cubingTwisty'; +import { ThemeProvider } from '../../theme'; import ScramblePreview, { puzzleByEvent } from './ScramblePreview'; jest.mock('../../lib/cubingTwisty', () => ({ @@ -11,6 +12,8 @@ jest.mock('../../lib/cubingTwisty', () => ({ const players = []; +const renderPreview = (element) => render({element}); + const flushPlayerLoad = () => act(async () => { await Promise.resolve(); }); @@ -37,7 +40,7 @@ it('maps every configured event to a cubing.js puzzle', () => { }); it('renders the static 2D end state of a scramble', async () => { - const { getByRole } = render( + const { getByRole } = renderPreview( , ); @@ -56,7 +59,7 @@ it('renders the static 2D end state of a scramble', async () => { }); it('enlarges the preview with its scramble text', async () => { - const { getByLabelText, getAllByRole, getByText } = render( + const { getByLabelText, getAllByRole, getByText } = renderPreview( , ); await flushPlayerLoad(); diff --git a/client/src/components/common/TableCellButton.jsx b/client/src/components/common/TableCellButton.jsx index bccea20..c67a799 100644 --- a/client/src/components/common/TableCellButton.jsx +++ b/client/src/components/common/TableCellButton.jsx @@ -1,9 +1,9 @@ import React from 'react'; import clsx from 'clsx'; import PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import TableCell from '@material-ui/core/TableCell'; -import Button from '@material-ui/core/Button'; +import { makeStyles } from '@mui/styles'; +import TableCell from '@mui/material/TableCell'; +import Button from '@mui/material/Button'; const useStyles = makeStyles(() => ({ root: { @@ -26,13 +26,13 @@ function TableCellButton({