chore: vite migration

This commit is contained in:
Mário Nunes
2024-05-31 07:45:14 +01:00
parent aa07934c84
commit 9cbcd55c74
156 changed files with 4171 additions and 5385 deletions

View File

@@ -106,7 +106,7 @@ commands:
- run: - run:
name: Set branch environment name: Set branch environment
command: | command: |
echo 'export REACT_APP_PROJECT_VERSION=${CIRCLE_SHA1}' >> $BASH_ENV echo 'export VITE_APP_PROJECT_VERSION=${CIRCLE_SHA1}' >> $BASH_ENV
- run: - run:
name: Inject target environment configuration name: Inject target environment configuration
command: | command: |
@@ -200,7 +200,6 @@ jobs:
resource_class: large resource_class: large
environment: environment:
CYPRESS_INSTALL_BINARY: 0 CYPRESS_INSTALL_BINARY: 0
JEST_JUNIT_OUTPUT_DIR: ./reports
steps: steps:
- setup_repo - setup_repo
- attach_workspace: - attach_workspace:
@@ -249,8 +248,6 @@ jobs:
- run: - run:
# NOTE - run-in-band to try reduce memory leaks (https://github.com/facebook/jest/issues/7874) # NOTE - run-in-band to try reduce memory leaks (https://github.com/facebook/jest/issues/7874)
command: yarn run test:unit && yarn run test:components command: yarn run test:unit && yarn run test:components
environment:
JEST_JUNIT_OUTPUT_DIR: reports
- store_artifacts: - store_artifacts:
path: coverage path: coverage
- store_artifacts: - store_artifacts:
@@ -288,13 +285,13 @@ jobs:
- run: - run:
name: Set branch environment name: Set branch environment
command: | command: |
echo 'export REACT_APP_BRANCH=${CIRCLE_BRANCH}' >> $BASH_ENV echo 'export VITE_APP_BRANCH=${CIRCLE_BRANCH}' >> $BASH_ENV
echo 'export REACT_APP_PROJECT_VERSION=${CIRCLE_SHA1}' >> $BASH_ENV echo 'export VITE_APP_PROJECT_VERSION=${CIRCLE_SHA1}' >> $BASH_ENV
- run: - run:
name: Check environment variables name: Check environment variables
command: | command: |
echo REACT_APP_BRANCH=$REACT_APP_BRANCH echo VITE_APP_BRANCH=$VITE_APP_BRANCH
echo $REACT_APP_PROJECT_VERSION echo $VITE_APP_PROJECT_VERSION
- run: - run:
command: yarn build command: yarn build
- persist_to_workspace: - persist_to_workspace:

View File

@@ -1,46 +0,0 @@
// Run a pre-flight check that developer environment setup in compatible way
const { envCheck } = require('./scripts/envCheck')
if (!process.env.CI) {
envCheck()
}
// Specific settings to use when running anything that requires a webpack compiler
// Enabled when npm command specifies `env-cmd -e webpack`
let webpack = {
NODE_OPTIONS: getNodeOptions(),
}
// Specific env to use with react-scripts / create-react-app
// Enabled when npm command specifies `env-cmd -e cra`
let cra = {
...webpack,
FAST_REFRESH: false,
}
exports.cra = cra
exports.webpack = webpack
/** Determine what node_options to provide depending on context */
function getNodeOptions() {
// Depending on node version use different environment variables to fix
// specific build or run issues
const NODE_VERSION = process.versions.node.split('.')[0]
let NODE_OPTIONS = process.env.NODE_OPTIONS || ''
// fix out-of-memory issues - default to 4GB but allow override from CI
// NOTE - would like to auto-calculate but does not support CI (https://github.com/nodejs/node/issues/27170)
if (!NODE_OPTIONS.includes('--max-old-space-size')) {
NODE_OPTIONS += ` --max-old-space-size=4096`
}
if (NODE_VERSION > '17') {
// fix https://github.com/facebook/create-react-app/issues/11708
// https://github.com/facebook/create-react-app/issues/12431
NODE_OPTIONS += ' --openssl-legacy-provider --no-experimental-fetch'
}
if (process.env.CI) {
console.log('NODE_OPTIONS', NODE_OPTIONS, '\n')
}
return NODE_OPTIONS.trim()
}

View File

@@ -8,7 +8,7 @@
"import", "import",
"simple-import-sort", "simple-import-sort",
"sort-class-members", "sort-class-members",
"jest" "vitest"
], ],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
@@ -104,7 +104,6 @@
"prefer": "type-imports" "prefer": "type-imports"
} }
], ],
"jest/no-focused-tests": "error",
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/interface-name-prefix": "off", "@typescript-eslint/interface-name-prefix": "off",
@@ -133,7 +132,7 @@
"camelCase": true, "camelCase": true,
"pascalCase": true "pascalCase": true
}, },
"ignore": ["react-app-env.d.ts", "service-worker.ts"] "ignore": ["vite-env.d.ts", "service-worker.ts"]
} }
], ],
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",

View File

@@ -79,7 +79,7 @@ jobs:
- name: Install npm dependencies - name: Install npm dependencies
run: yarn install --immutable run: yarn install --immutable
- name: Set environment variables - name: Set environment variables
run: export REACT_APP_PROJECT_VERSION=${GITHUB_SHA} run: export VITE_APP_PROJECT_VERSION=${GITHUB_SHA}
- name: Prepare Build - name: Prepare Build
run: yarn workspace oa-emulators-docker prepare run: yarn workspace oa-emulators-docker prepare
# Populate the list of build args generated in the prepare script to local env as multi line string # Populate the list of build args generated in the prepare script to local env as multi line string

View File

@@ -45,9 +45,9 @@ jobs:
- name: Install npm dependencies - name: Install npm dependencies
run: yarn install --immutable run: yarn install --immutable
- name: Set environment variables - name: Set environment variables
run: export REACT_APP_PROJECT_VERSION=${GITHUB_SHA} run: export VITE_APP_PROJECT_VERSION=${GITHUB_SHA}
- name: Check environment variables - name: Check environment variables
run: echo $REACT_APP_PROJECT_VERSION run: echo $VITE_APP_PROJECT_VERSION
- name: Build for Preview - name: Build for Preview
run: npm run build run: npm run build
env: env:
@@ -55,7 +55,7 @@ jobs:
# disable until fully resolved # disable until fully resolved
CI: false CI: false
# specify the 'preview' site variant to populate the relevant firebase config # specify the 'preview' site variant to populate the relevant firebase config
REACT_APP_SITE_VARIANT: preview VITE_APP_SITE_VARIANT: preview
# The hosting-deploy action calls firebase tools via npx, however installing globally # The hosting-deploy action calls firebase tools via npx, however installing globally
# gives us control over what version will be made available # gives us control over what version will be made available
- name: Install firebase-tools globally - name: Install firebase-tools globally

View File

@@ -10,6 +10,8 @@ nmMode: hardlinks-local
nodeLinker: node-modules nodeLinker: node-modules
checksumBehavior: update
packageExtensions: packageExtensions:
"@storybook/core-common@*": "@storybook/core-common@*":
dependencies: dependencies:

View File

@@ -1,5 +1,4 @@
/* global module */ export default {
module.exports = {
extends: ['@commitlint/config-conventional'], extends: ['@commitlint/config-conventional'],
/** Add optional custom formatter */ /** Add optional custom formatter */
// Temporarily removed due to unresolved error: https://app.circleci.com/pipelines/github/ONEARMY/community-platform/5891/workflows/59757a07-b416-43be-9a57-eedc1190d5a0/jobs/44654 // Temporarily removed due to unresolved error: https://app.circleci.com/pipelines/github/ONEARMY/community-platform/5891/workflows/59757a07-b416-43be-9a57-eedc1190d5a0/jobs/44654

View File

@@ -1,93 +0,0 @@
import { DefinePlugin, ProvidePlugin } from 'webpack'
import type { JestConfigOverride, WebpackConfigOverride } from '@craco/types'
import type { RuleSetRule } from 'webpack'
/**
* Craco is used to provide config overrides to the default webpack config that is called
* from react-scripts.
*/
module.exports = {
webpack: {
configure: (webpackConfig: WebpackConfigOverride['webpackConfig']) => {
// Add polyfills for node (mostly imports for pino-logflare)
// https://github.com/facebook/create-react-app/issues/11756
// https://stackoverflow.com/questions/68707553/uncaught-referenceerror-buffer-is-not-defined
webpackConfig.resolve!.fallback = {
stream: require.resolve('stream-browserify'),
buffer: require.resolve('buffer'),
}
webpackConfig.module!.rules = hackUpdateRulesToSupportCJS(
webpackConfig.module!.rules as RuleSetRule[],
)
webpackConfig.plugins = [
...(webpackConfig.plugins as any[]),
// Fix calls to process (pino-logflare and cypress calling db.ts outside of cra)
// NOTE - react creates 'process.env' variable but does not assign anything to 'process'
// https://github.com/facebook/create-react-app/issues/11951
new DefinePlugin({
process: {},
}),
new ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
]
// Fix sourcemap warning
// https://github.com/facebook/create-react-app/discussions/11767
webpackConfig.ignoreWarnings = [
function ignoreSourcemapsloaderWarnings(warning) {
return (
warning.module &&
(warning.module as any).resource.includes('node_modules') &&
warning.details &&
warning.details.includes('source-map-loader')
)
},
]
return webpackConfig
},
},
jest: {
configure: (jestConfig: JestConfigOverride['jestConfig']) => {
// https://kulshekhar.github.io/ts-jest/docs/getting-started/paths-mapping/
jestConfig.reporters = [
[
'jest-junit',
{ outputDirectory: 'reports', outputName: 'report.xml' },
],
]
jestConfig.moduleNameMapper = {
...jestConfig.moduleNameMapper,
'photoswipe/lightbox':
'<rootDir>/node_modules/photoswipe/dist/umd/photoswipe-lightbox.umd.min.js',
'photoswipe/style.css':
'<rootDir>/node_modules/photoswipe/dist/photoswipe.css',
// Allow specific import from 'src' (used to import `useCommonStores`)
'^src$': '<rootDir>/src/index',
}
return jestConfig
},
},
}
/**
* Prepend a custom rule to support CJS files
*
* NOTE - should be resolved in future CRA release pending merge of
* https://github.com/facebook/create-react-app/pull/12021
*/
const hackUpdateRulesToSupportCJS = (rules: RuleSetRule[]) => {
return rules.map((rule) => {
if (rule.oneOf instanceof Array) {
rule.oneOf[rule.oneOf.length - 1].exclude = [
/\.(js|mjs|jsx|cjs|ts|tsx)$/,
/\.html$/,
/\.json$/,
]
}
return rule
})
}

64
index.html Normal file
View File

@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<link rel="preconnect" href="https://storage.googleapis.com" crossorigin />
<link
rel="preconnect"
href="https://firebasestorage.googleapis.com"
crossorigin
/>
<noscript><link rel="stylesheet" href="path/to/stylesheet.css" /></noscript>
<meta name="theme-color" content="#000000" />
<meta property="og:title" content="Community Platform" />
<meta
property="og:description"
content="A series of tools for the Precious Plastic community to collaborate around the world. Connect, share and meet each other to tackle plastic waste."
/>
<meta property="og:image" content="./social-image.jpg" />
<meta property="og:url" content="https://community.preciousplastic.com" />
<meta name="twitter:title" content="Community Platform" />
<meta
name="twitter:description"
content="A series of tools for the Precious Plastic community to collaborate around the world. Connect, share and meet each other to tackle plastic waste."
/>
<meta name="twitter:image" content="./social-image.jpg" />
<meta name="twitter:card" content="summary_large_image" />
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json" />
<link id="favicon" rel="shortcut icon" href="./favicon.ico" />
<!--
Notice the use of ./public in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "./favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Precious Plastic Community</title>
<meta
name="description"
content="A series of tools for the Precious Plastic community to collaborate around the world. Connect, share and meet each other to tackle plastic waste."
/>
<script id="CommunityPlatform">
/**
* The following object has environment specific
* configuration injected into it as part of the build process
*/
window.__OA_COMMUNITY_PLATFORM_CONFIGURATION = {}
</script>
</head>
<body style="overflow-x: hidden">
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@@ -1,8 +0,0 @@
import type { JestConfigWithTsJest } from 'ts-jest'
const config: JestConfigWithTsJest = {
preset: 'ts-jest',
testEnvironment: 'node',
}
export default config

View File

@@ -12,22 +12,22 @@
], ],
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
"type": "module",
"scripts": { "scripts": {
"start": "concurrently --kill-others --names themes,components,platform --prefix-colors cyan,blue,magenta \"yarn start:themes\" \"yarn start:components\" \"yarn start:platform\"", "start": "concurrently --kill-others --names themes,components,platform --prefix-colors cyan,blue,magenta \"yarn start:themes\" \"yarn start:components\" \"yarn start:platform\"",
"start:themes": "yarn workspace oa-themes dev", "start:themes": "yarn workspace oa-themes dev",
"start:components": "yarn workspace oa-components dev", "start:components": "yarn workspace oa-components dev",
"start:platform": "yarn build:shared && env-cmd -e cra yarn craco start", "start:platform": "yarn build:shared && vite",
"start:emulated": "concurrently --kill-others --names functions,themes,components,platform --prefix-colors yellow,cyan,blue,magenta \"yarn workspace functions start\" \"yarn start:themes\" \"yarn start:components\" \"cross-env PORT=4000 yarn start:platform\"", "start:emulated": "concurrently --kill-others --names functions,themes,components,platform --prefix-colors yellow,cyan,blue,magenta \"yarn workspace functions start\" \"yarn start:themes\" \"yarn start:components\" \"cross-env PORT=4000 yarn start:platform\"",
"start:emulated:docker": "concurrently --names functions,themes,components,platform,emulators --prefix-colors yellow,cyan,blue,magenta,green --kill-others \"yarn workspace functions watch\" \"yarn start:themes\" \"yarn start:components\" \"cross-env PORT=4000 yarn start:platform\" \"yarn workspace oa-emulators-docker start\"", "start:emulated:docker": "concurrently --names functions,themes,components,platform,emulators --prefix-colors yellow,cyan,blue,magenta,green --kill-others \"yarn workspace functions watch\" \"yarn start:themes\" \"yarn start:components\" \"cross-env PORT=4000 yarn start:platform\" \"yarn workspace oa-emulators-docker start\"",
"start:emulated:docker:local": "concurrently --names functions,themes,components,platform,emulators --prefix-colors yellow,cyan,blue,magenta,green --kill-others \"yarn workspace functions watch\" \"yarn start:themes\" \"yarn start:components\" \"cross-env PORT=4000 yarn start:platform\" \"yarn workspace oa-emulators-docker start --repo=\"", "start:emulated:docker:local": "concurrently --names functions,themes,components,platform,emulators --prefix-colors yellow,cyan,blue,magenta,green --kill-others \"yarn workspace functions watch\" \"yarn start:themes\" \"yarn start:components\" \"cross-env PORT=4000 yarn start:platform\" \"yarn workspace oa-emulators-docker start --repo=\"",
"build:themes": "yarn workspace oa-themes build", "build:themes": "yarn workspace oa-themes build",
"build:components": "yarn workspace oa-components build", "build:components": "yarn workspace oa-components build",
"build:cra": "env-cmd -e cra craco build", "build:vite": "tsc && vite",
"build:post": "yarn workspace oa-scripts post-cra-build", "build:post": "yarn workspace oa-scripts post-cra-build",
"build:inject-config": "yarn build:post", "build:inject-config": "yarn build:post",
"build:shared": "yarn workspace oa-shared build", "build:shared": "yarn workspace oa-shared build",
"build": "yarn build:themes && yarn build:components && yarn build:shared && yarn build:cra", "build": "yarn build:themes && yarn build:components && yarn build:shared && yarn build:vite",
"eject": "react-scripts eject",
"lint": "yarn lint:style && yarn lint:code", "lint": "yarn lint:style && yarn lint:code",
"lint:commits": " npx commitlint --from=$(git merge-base master HEAD) --verbose", "lint:commits": " npx commitlint --from=$(git merge-base master HEAD) --verbose",
"lint:code": "eslint . --ext .js,.jsx,.ts,.tsx src --color", "lint:code": "eslint . --ext .js,.jsx,.ts,.tsx src --color",
@@ -39,7 +39,7 @@
"serve": "npx serve -s build", "serve": "npx serve -s build",
"test": "yarn workspace oa-cypress start", "test": "yarn workspace oa-cypress start",
"test:components": "yarn workspace oa-components test-ci", "test:components": "yarn workspace oa-components test-ci",
"test:unit": "yarn build:themes && yarn build:components && env-cmd -e cra craco test --env=jsdom --runInBand --logHeapUsage --coverage --reporters=default --reporters=jest-junit", "test:unit": "yarn build:themes && yarn build:components && vitest --logHeapUsage --coverage",
"test:madge": "npx madge --circular --extensions ts,tsx ./ --exclude src/stores", "test:madge": "npx madge --circular --extensions ts,tsx ./ --exclude src/stores",
"storybook": "yarn workspace oa-components start", "storybook": "yarn workspace oa-components start",
"storybook:build": "yarn build:themes && yarn workspace oa-components build:sb", "storybook:build": "yarn build:themes && yarn workspace oa-components build:sb",
@@ -75,9 +75,13 @@
"@emotion/react": "^11.10.6", "@emotion/react": "^11.10.6",
"@emotion/styled": "^11.8.1", "@emotion/styled": "^11.8.1",
"@sentry/react": "^6.15.0", "@sentry/react": "^6.15.0",
"@uppy/core": "^2.1.4", "@uppy/compressor": "^1.1.4",
"@uppy/dashboard": "^2.1.3", "@uppy/core": "^3.11.3",
"@uppy/react": "^2.1.2", "@uppy/dashboard": "^3.8.3",
"@uppy/drag-drop": "^3.1.0",
"@uppy/file-input": "^3.1.2",
"@uppy/progress-bar": "^3.1.1",
"@uppy/react": "^3.3.1",
"countries-list": "^2.6.1", "countries-list": "^2.6.1",
"date-fns": "^3.3.0", "date-fns": "^3.3.0",
"debounce": "^1.2.0", "debounce": "^1.2.0",
@@ -99,6 +103,7 @@
"oa-shared": "workspace:*", "oa-shared": "workspace:*",
"oa-themes": "workspace:*", "oa-themes": "workspace:*",
"react": "17.0.2", "react": "17.0.2",
"react-country-flag": "^3.1.0",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-dropzone": "^10.1.10", "react-dropzone": "^10.1.10",
"react-final-form": "6.5.3", "react-final-form": "6.5.3",
@@ -110,8 +115,6 @@
"react-leaflet-markercluster": "^2.0.0-rc3", "react-leaflet-markercluster": "^2.0.0-rc3",
"react-router": "^6.20.1", "react-router": "^6.20.1",
"react-router-dom": "^6.20.1", "react-router-dom": "^6.20.1",
"react-scripts": "5.0.1",
"react-virtualized": "9.22.5",
"rxjs": "^6.6.3", "rxjs": "^6.6.3",
"theme-ui": "^0.15.7", "theme-ui": "^0.15.7",
"ts-node": "^10.4.0", "ts-node": "^10.4.0",
@@ -119,20 +122,19 @@
"yup": "^1.3.2" "yup": "^1.3.2"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^16.2.3", "@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^16.2.1", "@commitlint/config-conventional": "^19.2.2",
"@commitlint/cz-commitlint": "^16.2.3", "@commitlint/cz-commitlint": "^19.2.0",
"@craco/craco": "^7.0.0", "@emotion/babel-plugin": "^11.11.0",
"@craco/types": "^7.0.0", "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.4.1",
"@semantic-release/changelog": "^6.0.1", "@semantic-release/changelog": "^6.0.1",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.3",
"@types/browser-image-compression": "^1.0.9", "@types/browser-image-compression": "^1.0.9",
"@types/flux-standard-action": "1.1.0", "@types/flux-standard-action": "1.1.0",
"@types/jest": "^27.4.1",
"@types/node": "^20.8.0", "@types/node": "^20.8.0",
"@types/pubsub-js": "^1.5.18", "@types/pubsub-js": "^1.5.18",
"@types/react": "17.0.2", "@types/react": "17.0.2",
@@ -141,10 +143,11 @@
"@types/react-leaflet-markercluster": "^2.0.0", "@types/react-leaflet-markercluster": "^2.0.0",
"@types/react-router-dom": "5.3.3", "@types/react-router-dom": "5.3.3",
"@types/react-select": "^2.0.17", "@types/react-select": "^2.0.17",
"@types/react-virtualized": "^9.21.10",
"@types/styled-system": "^5.1.11", "@types/styled-system": "^5.1.11",
"@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3", "@typescript-eslint/parser": "^6.7.3",
"@vitejs/plugin-react": "^4.3.0",
"@vitest/coverage-v8": "^1.6.0",
"all-contributors-cli": "^6.20.0", "all-contributors-cli": "^6.20.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"chai-subset": "^1.6.0", "chai-subset": "^1.6.0",
@@ -158,16 +161,16 @@
"eslint-import-resolver-typescript": "^3.6.1", "eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-cypress": "^2.15.1", "eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^27.4.2",
"eslint-plugin-mocha": "^10.2.0", "eslint-plugin-mocha": "^10.2.0",
"eslint-plugin-prefer-arrow-functions": "^3.1.4", "eslint-plugin-prefer-arrow-functions": "^3.1.4",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "^7.34.2",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-sort-class-members": "^1.18.0", "eslint-plugin-sort-class-members": "^1.18.0",
"eslint-plugin-unicorn": "^48.0.1", "eslint-plugin-unicorn": "^48.0.1",
"husky": "^7.0.4", "eslint-plugin-vitest": "^0.5.4",
"husky": "^9.0.11",
"idb": "^6.0.0", "idb": "^6.0.0",
"jest-junit": "^16.0.0",
"lint-staged": "^8.1.5", "lint-staged": "^8.1.5",
"mobx-react-devtools": "^6.0.3", "mobx-react-devtools": "^6.0.3",
"prettier": "2.5.1", "prettier": "2.5.1",
@@ -176,9 +179,13 @@
"start-server-and-test": "^1.11.0", "start-server-and-test": "^1.11.0",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"terser": "3.14.1", "terser": "3.14.1",
"ts-jest": "^29.1.2",
"ts-loader": "^7.0.5", "ts-loader": "^7.0.5",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"vite": "^5.2.12",
"vite-plugin-env-compatible": "^2.0.1",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0",
"wait-on": "^5.2.1", "wait-on": "^5.2.1",
"workbox-background-sync": "^6.1.5", "workbox-background-sync": "^6.1.5",
"workbox-broadcast-update": "^6.1.5", "workbox-broadcast-update": "^6.1.5",

View File

@@ -5,6 +5,7 @@
"private": true, "private": true,
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.js", "types": "dist/index.d.js",
"type": "module",
"scripts": { "scripts": {
"storybook": "yarn start", "storybook": "yarn start",
"start": "storybook dev -p 6006 --loglevel verbose", "start": "storybook dev -p 6006 --loglevel verbose",
@@ -32,7 +33,7 @@
"react-flag-icon-css": "^1.0.25", "react-flag-icon-css": "^1.0.25",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-image-crop": "^11.0.5", "react-image-crop": "^11.0.5",
"react-player": "^2.12.0", "react-player": "^2.16.0",
"react-portal": "^4.2.2", "react-portal": "^4.2.2",
"react-router": "6.20.1", "react-router": "6.20.1",
"react-router-dom": "^6.20.1", "react-router-dom": "^6.20.1",
@@ -62,12 +63,13 @@
"@types/mustache": "^4.1.2", "@types/mustache": "^4.1.2",
"@types/react-flag-icon-css": "^1.0.5", "@types/react-flag-icon-css": "^1.0.5",
"@types/react-portal": "^4.0.4", "@types/react-portal": "^4.0.4",
"@vitejs/plugin-react": "^3.1.0", "@vitejs/plugin-react": "^4.3.0",
"@vitest/coverage-c8": "^0.30.1", "@vitest/coverage-v8": "^1.6.0",
"babel-loader": "8.1.0", "babel-loader": "8.1.0",
"eslint": "^8.50.0", "eslint": "^8.50.0",
"eslint-plugin-import": "^2.28.1", "eslint-plugin-import": "^2.28.1",
"eslint-plugin-storybook": "^0.6.13", "eslint-plugin-storybook": "^0.6.13",
"eslint-plugin-vitest": "^0.5.4",
"jsdom": "^21.1.1", "jsdom": "^21.1.1",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"prettier": "2.5.1", "prettier": "2.5.1",
@@ -75,6 +77,6 @@
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"ts-node": "^10.7.0", "ts-node": "^10.7.0",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"vitest": "^0.30.1" "vitest": "^1.6.0"
} }
} }

View File

@@ -1,3 +1,7 @@
import '@testing-library/jest-dom'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { Default, NoCategory } from './Breadcrumbs.stories' import { Default, NoCategory } from './Breadcrumbs.stories'

View File

@@ -1,3 +1,7 @@
import '@testing-library/jest-dom'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { import {
Default, Default,

View File

@@ -1,5 +1,5 @@
import { fireEvent } from '@testing-library/react' import { fireEvent } from '@testing-library/react'
import { vi } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { createFakeComments, fakeComment } from '../utils' import { createFakeComments, fakeComment } from '../utils'

View File

@@ -1,3 +1,5 @@
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { Default } from './ContentStatistics.stories' import { Default } from './ContentStatistics.stories'

View File

@@ -1,5 +1,7 @@
import '@testing-library/jest-dom'
import { fireEvent } from '@testing-library/react' import { fireEvent } from '@testing-library/react'
import { vi } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { CreateComment } from './CreateComment' import { CreateComment } from './CreateComment'

View File

@@ -1,4 +1,7 @@
import '@testing-library/jest-dom'
import { fireEvent, waitFor } from '@testing-library/react' import { fireEvent, waitFor } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { Default, LoggedIn, LoggedInWithError } from './CreateReply.stories' import { Default, LoggedIn, LoggedInWithError } from './CreateReply.stories'

View File

@@ -1,5 +1,8 @@
import '@testing-library/jest-dom'
import { act } from 'react-dom/test-utils' import { act } from 'react-dom/test-utils'
import { fireEvent } from '@testing-library/react' import { fireEvent } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { Default, WithReplies } from './DiscussionContainer.stories' import { Default, WithReplies } from './DiscussionContainer.stories'

View File

@@ -1,3 +1,7 @@
import '@testing-library/jest-dom'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { COMMENTS, NO_COMMENTS, ONE_COMMENT } from './DiscussionTitle' import { COMMENTS, NO_COMMENTS, ONE_COMMENT } from './DiscussionTitle'
import { import {

View File

@@ -1,5 +1,5 @@
import { fireEvent } from '@testing-library/react' import { fireEvent } from '@testing-library/react'
import { vi } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { DonationRequest, REQUEST_BUTTON_SKIP } from './DonationRequest' import { DonationRequest, REQUEST_BUTTON_SKIP } from './DonationRequest'

View File

@@ -1,4 +1,6 @@
import { vi } from 'vitest' import '@testing-library/jest-dom'
import { describe, expect, it, vi } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { DonationRequestModal } from './DonationRequestModal' import { DonationRequestModal } from './DonationRequestModal'

View File

@@ -1,3 +1,7 @@
import '@testing-library/jest-dom'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { Default } from './DownloadButton.stories' import { Default } from './DownloadButton.stories'

View File

@@ -1,3 +1,7 @@
import '@testing-library/jest-dom'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { Default, One, Zero } from './DownloadCounter.stories' import { Default, One, Zero } from './DownloadCounter.stories'

View File

@@ -1,3 +1,7 @@
import '@testing-library/jest-dom'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { Default } from './DownloadFileFromLink.stories' import { Default } from './DownloadFileFromLink.stories'

View File

@@ -1,5 +1,7 @@
import '@testing-library/jest-dom'
import { act, fireEvent } from '@testing-library/react' import { act, fireEvent } from '@testing-library/react'
import { vi } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { EditComment, type IProps } from './EditComment' import { EditComment, type IProps } from './EditComment'

View File

@@ -1,30 +1,16 @@
import * as React from 'react' import * as React from 'react'
import { ReactCountryFlag } from 'react-country-flag'
import styled from '@emotion/styled' import styled from '@emotion/styled'
import { Box } from 'theme-ui' import { Box } from 'theme-ui'
import FlagIconFactory from 'react-flag-icon-css' export const FlagIconEvents = styled(ReactCountryFlag)`
// Please only use `FlagIconFactory` one time in your application, there is no
// need to use it multiple times (it would slow down your app). You may place the
// line below in a `FlagIcon.js` file in your 'components' directory, then
// write `export default FlagIcon` as shown below and import it elsewhere in your app.
export const FlagIconFact = FlagIconFactory(React, { useCssModules: false })
export const FlagIconEvents = styled(FlagIconFact)`
border-radius: 5px; border-radius: 5px;
background-size: cover !important; background-size: cover !important;
height: 23px; height: 23px;
width: 35px !important; width: 35px !important;
` `
/*
@media only screen and (max-width: ${(props) => props.theme.breakpoints[1]}) { export const FlagIconHowTos = styled(ReactCountryFlag)`
height: 15px;
width: 25px !important;
}
*/
export const FlagIconHowTos = styled(FlagIconFact)`
border-radius: 3px; border-radius: 3px;
background-size: cover !important; background-size: cover !important;
height: 14px; height: 14px;
@@ -33,6 +19,8 @@ export const FlagIconHowTos = styled(FlagIconFact)`
export const FlagIcon = (props: any) => ( export const FlagIcon = (props: any) => (
<Box {...(props as any)}> <Box {...(props as any)}>
<FlagIconEvents code={props.code}>{props.children}</FlagIconEvents> <FlagIconEvents countryCode={props.code} title={props.code} svg={true}>
{props.children}
</FlagIconEvents>
</Box> </Box>
) )

View File

@@ -1,3 +1,5 @@
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { Default } from './IconCountWithTooltip.stories' import { Default } from './IconCountWithTooltip.stories'

View File

@@ -1,5 +1,7 @@
import '@testing-library/jest-dom'
import { fireEvent } from '@testing-library/react' import { fireEvent } from '@testing-library/react'
import { vi } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { ImageCrop } from './ImageCrop' import { ImageCrop } from './ImageCrop'

View File

@@ -1,7 +1,7 @@
// An edited version of https://codesandbox.io/p/sandbox/react-image-crop-demo-with-react-hooks-y831o // An edited version of https://codesandbox.io/p/sandbox/react-image-crop-demo-with-react-hooks-y831o
import React, { useRef, useState } from 'react' import React, { useRef, useState } from 'react'
import ReactCrop from 'react-image-crop' import { ReactCrop } from 'react-image-crop'
import { Box, Flex, Text } from 'theme-ui' import { Box, Flex, Text } from 'theme-ui'
import { Button } from '../Button/Button' import { Button } from '../Button/Button'

View File

@@ -1,5 +1,7 @@
import '@testing-library/jest-dom/vitest'
import { findByRole as globalFindByRole, waitFor } from '@testing-library/react' import { findByRole as globalFindByRole, waitFor } from '@testing-library/react'
import { vi } from 'vitest' import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { ImageGallery } from './ImageGallery' import { ImageGallery } from './ImageGallery'

View File

@@ -1,3 +1,7 @@
import '@testing-library/jest-dom'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { MapMemberCard } from './MapMemberCard' import { MapMemberCard } from './MapMemberCard'
import { Default, ModerationComments } from './MapMemberCard.stories' import { Default, ModerationComments } from './MapMemberCard.stories'

View File

@@ -3,14 +3,9 @@ import { Box, Flex } from 'theme-ui'
import { Icon } from '../Icon/Icon' import { Icon } from '../Icon/Icon'
import type { NotificationType } from 'oa-shared' import type { NotificationType, UserNotificationItem } from 'oa-shared'
import type { availableGlyphs } from '../Icon/types' import type { availableGlyphs } from '../Icon/types'
export interface UserNotificationItem {
type: NotificationType
children: React.ReactNode
}
function getIconByType(type: NotificationType): availableGlyphs { function getIconByType(type: NotificationType): availableGlyphs {
if (['howto_useful', 'research_useful'].includes(type)) return 'useful' if (['howto_useful', 'research_useful'].includes(type)) return 'useful'
if ( if (

View File

@@ -4,7 +4,7 @@ import { InternalLink } from '../InternalLink/InternalLink'
import { NotificationList } from './NotificationList' import { NotificationList } from './NotificationList'
import type { Meta, StoryFn } from '@storybook/react' import type { Meta, StoryFn } from '@storybook/react'
import type { UserNotificationItem } from '../NotificationItem/NotificationItem' import type { UserNotificationItem } from 'oa-shared'
export default { export default {
title: 'Components/NotificationList', title: 'Components/NotificationList',

View File

@@ -5,8 +5,8 @@ import { Box, Card, Text } from 'theme-ui'
import { Button } from '../Button/Button' import { Button } from '../Button/Button'
import { NotificationItem } from '../NotificationItem/NotificationItem' import { NotificationItem } from '../NotificationItem/NotificationItem'
import type { UserNotificationItem } from 'oa-shared'
import type { ThemeUIStyleObject } from 'theme-ui' import type { ThemeUIStyleObject } from 'theme-ui'
import type { UserNotificationItem } from '../NotificationItem/NotificationItem'
export interface Props { export interface Props {
notifications: UserNotificationItem[] notifications: UserNotificationItem[]

View File

@@ -1,3 +1,7 @@
import '@testing-library/jest-dom'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { Facebook, Instagram, Twitter, Youtube } from './ProfileLink.stories' import { Facebook, Instagram, Twitter, Youtube } from './ProfileLink.stories'

View File

@@ -1,3 +1,7 @@
import '@testing-library/jest-dom'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { import {
Default, Default,

View File

@@ -1,4 +1,7 @@
import '@testing-library/jest-dom'
import { act } from '@testing-library/react' import { act } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { Default } from './TabbedContent.stories' import { Default } from './TabbedContent.stories'

View File

@@ -1,3 +1,7 @@
import '@testing-library/jest-dom'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { TagList } from './TagList' import { TagList } from './TagList'

View File

@@ -1,3 +1,7 @@
import '@testing-library/jest-dom'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { Default } from './UserEngagementWrapper.stories' import { Default } from './UserEngagementWrapper.stories'

View File

@@ -1,3 +1,7 @@
import '@testing-library/jest-dom/vitest'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { Default } from './UserStatistics.stories' import { Default } from './UserStatistics.stories'

View File

@@ -1,3 +1,7 @@
import '@testing-library/jest-dom'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { Username } from './Username' import { Username } from './Username'
import { import {

View File

@@ -59,7 +59,11 @@ export const Username = ({ user, sx }: IProps) => {
<Flex mr={1} sx={{ display: 'inline-flex' }}> <Flex mr={1} sx={{ display: 'inline-flex' }}>
{countryCode && isValidCountryCode(countryCode) ? ( {countryCode && isValidCountryCode(countryCode) ? (
<Flex data-testid="Username: known flag"> <Flex data-testid="Username: known flag">
<FlagIconHowTos code={countryCode.toLowerCase()} /> <FlagIconHowTos
countryCode={countryCode}
svg={true}
title={countryCode}
/>
</Flex> </Flex>
) : ( ) : (
<Flex <Flex

View File

@@ -1,3 +1,7 @@
import '@testing-library/jest-dom'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils' import { render } from '../tests/utils'
import { Youtube } from './VideoPlayer.stories' import { Youtube } from './VideoPlayer.stories'

View File

@@ -1,4 +1,4 @@
import ReactPlayer from 'react-player/lazy' import ReactPlayer from 'react-player'
import { Box } from 'theme-ui' import { Box } from 'theme-ui'
export interface Props { export interface Props {

View File

@@ -57,4 +57,3 @@ export { UserEngagementWrapper } from './UserEngagementWrapper/UserEngagementWra
export { IconCountWithTooltip } from './IconCountWithTooltip/IconCountWithTooltip' export { IconCountWithTooltip } from './IconCountWithTooltip/IconCountWithTooltip'
export { DonationRequest } from './DonationRequest/DonationRequest' export { DonationRequest } from './DonationRequest/DonationRequest'
export { DonationRequestModal } from './DonationRequestModal/DonationRequestModal' export { DonationRequestModal } from './DonationRequestModal/DonationRequestModal'
export { UserNotificationItem } from './NotificationItem/NotificationItem'

View File

@@ -1,16 +0,0 @@
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
// eslint-disable-next-line import/no-default-export
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/tests/setup.ts',
coverage: {
provider: 'c8',
reporter: ['text', 'json', 'html'],
},
},
})

View File

@@ -0,0 +1,24 @@
import react from '@vitejs/plugin-react'
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import svgr from 'vite-plugin-svgr'
import type { UserConfig as VitestUserConfigInterface } from 'vitest/config'
const vitestConfig: VitestUserConfigInterface = {
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/test/setup.ts',
coverage: {
provider: 'v8',
},
reporters: ['junit'],
include: ['./src/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
},
}
// eslint-disable-next-line import/no-default-export
export default defineConfig({
plugins: [react(), svgr()],
test: vitestConfig.test,
})

View File

@@ -54,7 +54,7 @@ async function main() {
async function startAppServer() { async function startAppServer() {
const { CROSSENV_BIN, BUILD_SERVE_JSON } = PATHS const { CROSSENV_BIN, BUILD_SERVE_JSON } = PATHS
// by default spawns will not respect colours used in stdio, so try to force // by default spawns will not respect colours used in stdio, so try to force
const crossEnvArgs = `FORCE_COLOR=1 REACT_APP_SITE_VARIANT=test-ci` const crossEnvArgs = `FORCE_COLOR=1 VITE_APP_SITE_VARIANT=test-ci`
// run local debug server for testing unless production build specified // run local debug server for testing unless production build specified
let serverCmd = `${CROSSENV_BIN} ${crossEnvArgs} BROWSER=none PORT=3456 yarn start` let serverCmd = `${CROSSENV_BIN} ${crossEnvArgs} BROWSER=none PORT=3456 yarn start`

View File

@@ -11,7 +11,7 @@ const e = process.env || ({} as any)
* e.g. oa_ * e.g. oa_
* SessionStorage prefixes are used to allow test ci environments to dynamically set a db endpoint * SessionStorage prefixes are used to allow test ci environments to dynamically set a db endpoint
*/ */
const DB_PREFIX = sessionStorage.DB_PREFIX || e.REACT_APP_DB_PREFIX || '' const DB_PREFIX = sessionStorage.DB_PREFIX || e.VITE_APP_DB_PREFIX || ''
/** /**
* Mapping of generic database endpoints to specific prefixed and revisioned versions for the * Mapping of generic database endpoints to specific prefixed and revisioned versions for the

View File

@@ -44,29 +44,29 @@ See [circleci slack orb](https://github.com/CircleCI-Public/slack-orb) for info)
### Runtime Variables ### Runtime Variables
Any variables prefixed with `REACT_APP_` are automatically included with the runtime build. Currently we require: Any variables prefixed with `VITE_APP_` are automatically included with the runtime build. Currently we require:
Firebase configuration Firebase configuration
``` ```
REACT_APP_FIREBASE_API_KEY VITE_APP_FIREBASE_API_KEY
REACT_APP_FIREBASE_AUTH_DOMAIN VITE_APP_FIREBASE_AUTH_DOMAIN
REACT_APP_FIREBASE_DATABASE_URL VITE_APP_FIREBASE_DATABASE_URL
REACT_APP_FIREBASE_MESSAGING_SENDER_ID VITE_APP_FIREBASE_MESSAGING_SENDER_ID
REACT_APP_FIREBASE_PROJECT_ID VITE_APP_FIREBASE_PROJECT_ID
REACT_APP_FIREBASE_STORAGE_BUCKET VITE_APP_FIREBASE_STORAGE_BUCKET
``` ```
Sentry error tracking Sentry error tracking
``` ```
REACT_APP_SENTRY_DSN VITE_APP_SENTRY_DSN
``` ```
Google Analytics Google Analytics
``` ```
REACT_APP_GA_TRACKING_ID VITE_APP_GA_TRACKING_ID
``` ```
### Misc Variables ### Misc Variables

View File

@@ -38,17 +38,17 @@ You will need to set up a CircleCI context for each target environment. This con
- `FIREBASE_TOKEN` - `FIREBASE_TOKEN`
- `GOOGLE_APPLICATION_CREDENTIALS_JSON` - `GOOGLE_APPLICATION_CREDENTIALS_JSON`
- `REACT_APP_BRANCH` - `VITE_APP_BRANCH`
- `REACT_APP_FIREBASE_API_KEY` - `VITE_APP_FIREBASE_API_KEY`
- `REACT_APP_FIREBASE_AUTH_DOMAIN` - `VITE_APP_FIREBASE_AUTH_DOMAIN`
- `REACT_APP_FIREBASE_DATABASE_URL` - `VITE_APP_FIREBASE_DATABASE_URL`
- `REACT_APP_FIREBASE_MESSAGING_SENDER_ID` - `VITE_APP_FIREBASE_MESSAGING_SENDER_ID`
- `REACT_APP_FIREBASE_PROJECT_ID` - `VITE_APP_FIREBASE_PROJECT_ID`
- `REACT_APP_FIREBASE_STORAGE_BUCKET` - `VITE_APP_FIREBASE_STORAGE_BUCKET`
- `REACT_APP_GA_TRACKING_ID` - `VITE_APP_GA_TRACKING_ID`
- `REACT_APP_PLATFORM_THEME` - `VITE_APP_PLATFORM_THEME`
- `REACT_APP_CDN_URL` - `https://cdn-url.com` - this is the URL to the CDN where the assets are stored. This is used to load the assets from the CDN instead of the local server. It should **not** include a trailing slash. - `VITE_APP_CDN_URL` - `https://cdn-url.com` - this is the URL to the CDN where the assets are stored. This is used to load the assets from the CDN instead of the local server. It should **not** include a trailing slash.
- `REACT_APP_PLATFORM_PROFILES` - comma separated list of available profiles. Use `ProfileType` from modules/profile/index for guidance here. For example: `member,workspace` - `VITE_APP_PLATFORM_PROFILES` - comma separated list of available profiles. Use `ProfileType` from modules/profile/index for guidance here. For example: `member,workspace`
- `REACT_APP_SUPPORTED_MODULES`  comma separated list of available modules. See `/src/modules/index.ts` for the definitions. - `VITE_APP_SUPPORTED_MODULES`  comma separated list of available modules. See `/src/modules/index.ts` for the definitions.
- `REACT_APP_API_URL` 'https://api-url.com' - this is the URL to the API service. It should **not** include a trailing slash. - `VITE_APP_API_URL` 'https://api-url.com' - this is the URL to the API service. It should **not** include a trailing slash.
- `SITE_NAME` - `SITE_NAME`

View File

@@ -1,4 +1,6 @@
require('dotenv').config() import dotenv from 'dotenv'
dotenv.config()
import * as testing from '@firebase/rules-unit-testing' import * as testing from '@firebase/rules-unit-testing'
import { doc, getDoc, setDoc, setLogLevel } from 'firebase/firestore' import { doc, getDoc, setDoc, setLogLevel } from 'firebase/firestore'

View File

@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "CommonJS", "module": "ESNext",
"lib": ["ESNext"], "lib": ["ESNext"],
"types": ["node"], "types": ["node"],
"moduleResolution": "Node", "moduleResolution": "Node",

View File

@@ -55,7 +55,7 @@ const storage =
* e.g. oa_ * e.g. oa_
* SessionStorage prefixes are used to allow test ci environments to dynamically set a db endpoint * SessionStorage prefixes are used to allow test ci environments to dynamically set a db endpoint
*/ */
const DB_PREFIX = storage.DB_PREFIX || e.REACT_APP_DB_PREFIX || '' const DB_PREFIX = storage.DB_PREFIX || e.VITE_APP_DB_PREFIX || ''
/** /**
* Mapping of generic database endpoints to specific prefixed and revisioned versions for the * Mapping of generic database endpoints to specific prefixed and revisioned versions for the

View File

@@ -23,3 +23,8 @@ export const NotificationTypes = [
] as const ] as const
export type NotificationType = typeof NotificationTypes[number] export type NotificationType = typeof NotificationTypes[number]
export type UserNotificationItem = {
type: NotificationType
children: React.ReactNode
}

View File

@@ -1,6 +1,7 @@
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import { UserRole } from 'oa-shared' import { UserRole } from 'oa-shared'
import { FactoryUser } from 'src/test/factories/User' import { FactoryUser } from 'src/test/factories/User'
import { describe, expect, it, vi } from 'vitest'
import { AuthWrapper } from './AuthWrapper' // adjust this import according to your file structure import { AuthWrapper } from './AuthWrapper' // adjust this import according to your file structure
@@ -10,7 +11,7 @@ const mockUser = FactoryUser({
userRoles: [UserRole.ADMIN], userRoles: [UserRole.ADMIN],
}) })
jest.mock('src/common/hooks/useCommonStores', () => ({ vi.mock('src/common/hooks/useCommonStores', () => ({
__esModule: true, __esModule: true,
useCommonStores: () => ({ useCommonStores: () => ({
stores: { stores: {

View File

@@ -1,8 +1,11 @@
import '@testing-library/jest-dom/vitest'
import { ThemeProvider } from '@emotion/react' import { ThemeProvider } from '@emotion/react'
import { act, render, waitFor } from '@testing-library/react' import { act, render, waitFor } from '@testing-library/react'
import { Provider } from 'mobx-react' import { Provider } from 'mobx-react'
import { FactoryUser } from 'src/test/factories/User' import { FactoryUser } from 'src/test/factories/User'
import { testingThemeStyles } from 'src/test/utils/themeUtils' import { testingThemeStyles } from 'src/test/utils/themeUtils'
import { describe, expect, it, vi } from 'vitest'
import { DiscussionWrapper } from './DiscussionWrapper' import { DiscussionWrapper } from './DiscussionWrapper'
@@ -11,12 +14,12 @@ import type { IDiscussion } from 'src/models'
const Theme = testingThemeStyles const Theme = testingThemeStyles
const mockUser = FactoryUser() const mockUser = FactoryUser()
jest.mock('src/common/hooks/useCommonStores', () => ({ vi.mock('src/common/hooks/useCommonStores', () => ({
__esModule: true, __esModule: true,
useCommonStores: () => ({ useCommonStores: () => ({
stores: { stores: {
discussionStore: { discussionStore: {
fetchOrCreateDiscussionBySource: () => jest.fn(), fetchOrCreateDiscussionBySource: () => vi.fn(),
activeUser: () => mockUser, activeUser: () => mockUser,
}, },
}, },
@@ -29,7 +32,7 @@ describe('DiscussionWrapper', () => {
const discussionProps = { const discussionProps = {
sourceType: 'question' as IDiscussion['sourceType'], sourceType: 'question' as IDiscussion['sourceType'],
sourceId: '82364tdf', sourceId: '82364tdf',
setTotalCommentsCount: () => jest.fn(), setTotalCommentsCount: () => vi.fn(),
} }
let wrapper let wrapper

View File

@@ -1,18 +1,21 @@
import '@testing-library/jest-dom/vitest'
import { fireEvent, render } from '@testing-library/react' import { fireEvent, render } from '@testing-library/react'
import { UserRole } from 'oa-shared' import { UserRole } from 'oa-shared'
import { FactoryUser } from 'src/test/factories/User' import { FactoryUser } from 'src/test/factories/User'
import { describe, expect, it, vi } from 'vitest'
import { useCommonStores } from './hooks/useCommonStores' import { useCommonStores } from './hooks/useCommonStores'
import { DownloadWithDonationAsk } from './DownloadWithDonationAsk' import { DownloadWithDonationAsk } from './DownloadWithDonationAsk'
const mockedUsedNavigate = jest.fn() const mockedUsedNavigate = vi.fn()
jest.mock('react-router-dom', () => ({ vi.mock('react-router-dom', () => ({
useNavigate: () => mockedUsedNavigate, useNavigate: () => mockedUsedNavigate,
})) }))
jest.mock('src/common/hooks/useCommonStores', () => ({ vi.mock('src/common/hooks/useCommonStores', () => ({
__esModule: true, __esModule: true,
useCommonStores: jest.fn(), useCommonStores: vi.fn(),
})) }))
const userToMock = (user) => { const userToMock = (user) => {
return useCommonStores.mockImplementation(() => ({ return useCommonStores.mockImplementation(() => ({
@@ -24,7 +27,7 @@ describe('DownloadFileFromLink', () => {
it('when logged out, requires users to login', () => { it('when logged out, requires users to login', () => {
const { getAllByTestId } = render( const { getAllByTestId } = render(
<DownloadWithDonationAsk <DownloadWithDonationAsk
handleClick={jest.fn()} handleClick={vi.fn()}
isLoggedIn={false} isLoggedIn={false}
link="http://youtube.com/" link="http://youtube.com/"
/>, />,
@@ -40,7 +43,7 @@ describe('DownloadFileFromLink', () => {
const user = FactoryUser() const user = FactoryUser()
userToMock(user) userToMock(user)
const handleClick = jest.fn() const handleClick = vi.fn()
const { getAllByTestId } = render( const { getAllByTestId } = render(
<DownloadWithDonationAsk <DownloadWithDonationAsk
handleClick={handleClick} handleClick={handleClick}
@@ -59,7 +62,7 @@ describe('DownloadFileFromLink', () => {
const user = FactoryUser({ userRoles: [UserRole.BETA_TESTER] }) const user = FactoryUser({ userRoles: [UserRole.BETA_TESTER] })
userToMock(user) userToMock(user)
const handleClick = jest.fn() const handleClick = vi.fn()
const { getAllByTestId } = render( const { getAllByTestId } = render(
<DownloadWithDonationAsk <DownloadWithDonationAsk

View File

@@ -1,4 +1,5 @@
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { ErrorsContainer } from './ErrorsContainer' import { ErrorsContainer } from './ErrorsContainer'

View File

@@ -1,5 +1,6 @@
import * as React from 'react' import * as React from 'react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import Compressor from '@uppy/compressor'
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import { DashboardModal } from '@uppy/react' import { DashboardModal } from '@uppy/react'
import { Button, DownloadStaticFile } from 'oa-components' import { Button, DownloadStaticFile } from 'oa-components'
@@ -24,8 +25,10 @@ interface IState {
} }
export const FileInput = (props: IProps) => { export const FileInput = (props: IProps) => {
const [state, setState] = useState<IState>({ open: false }) const [state, setState] = useState<IState>({ open: false })
const [uppy] = useState( const [uppy] = useState(() =>
() => new Uppy({ ...UPPY_CONFIG, onBeforeUpload: () => uploadTriggered() }), new Uppy({ ...UPPY_CONFIG, onBeforeUpload: () => uploadTriggered() }).use(
Compressor,
),
) )
useEffect(() => { useEffect(() => {

View File

@@ -1,4 +1,7 @@
import '@testing-library/jest-dom/vitest'
import { fireEvent, render } from '@testing-library/react' import { fireEvent, render } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { HideDiscussionContainer } from './HideDiscussionContainer' import { HideDiscussionContainer } from './HideDiscussionContainer'

View File

@@ -1,19 +1,22 @@
import { act } from 'react-dom/test-utils' import { act } from 'react-dom/test-utils'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { ScrollToTop } from './ScrollToTop' import { ScrollToTop } from './ScrollToTop'
jest.mock('react-router-dom', () => ({ import type { Mock } from 'vitest'
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(), vi.mock('react-router-dom', async () => ({
...(await vi.importActual('react-router-dom')),
useLocation: vi.fn(),
})) }))
describe('ScrollToTop', () => { describe('ScrollToTop', () => {
it('should scroll to top when pathname changes', async () => { it('should scroll to top when pathname changes', async () => {
const scrollToSpy = jest.fn() const scrollToSpy = vi.fn()
global.window.scrollTo = scrollToSpy global.window.scrollTo = scrollToSpy
;(useLocation as jest.Mock).mockImplementation(() => ({ ;(useLocation as Mock).mockImplementation(() => ({
pathname: '/initial', pathname: '/initial',
})) }))
@@ -27,7 +30,7 @@ describe('ScrollToTop', () => {
// Reset the mock to track subsequent calls // Reset the mock to track subsequent calls
scrollToSpy.mockReset() scrollToSpy.mockReset()
;(useLocation as jest.Mock).mockImplementation(() => ({ ;(useLocation as Mock).mockImplementation(() => ({
pathname: '/changed', pathname: '/changed',
})) }))

View File

@@ -1,8 +1,11 @@
import '@testing-library/jest-dom/vitest'
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { TagList } from './TagsList' import { TagList } from './TagsList'
jest.mock('src/common/hooks/useCommonStores', () => ({ vi.mock('src/common/hooks/useCommonStores', () => ({
__esModule: true, __esModule: true,
useCommonStores: () => ({ useCommonStores: () => ({
stores: { stores: {

View File

@@ -4,7 +4,7 @@ Switch config dependent on use case
For our use case the production config is stored in environment variables passed from For our use case the production config is stored in environment variables passed from
CI. You can replace this with your own config or use the same pattern to keep CI. You can replace this with your own config or use the same pattern to keep
api keys secret. Note, create-react-app only passes environment variables prefixed with api keys secret. Note, create-react-app only passes environment variables prefixed with
'REACT_APP'. The required info has been encrypted and stored in a circleCI deployment context. 'VITE_APP'. The required info has been encrypted and stored in a circleCI deployment context.
Dev config is hardcoded - You can find more information about potential security risk here: Dev config is hardcoded - You can find more information about potential security risk here:
https://javebratt.com/hide-firebase-api/ https://javebratt.com/hide-firebase-api/
@@ -17,7 +17,7 @@ import type { IFirebaseConfig, ISentryConfig, siteVariants } from './types'
/** /**
* Helper function to load configuration property * Helper function to load configuration property
* from the global configuration object * from the global configuration object
* During the development cycle this will be process.env * During the development cycle this will be import.meta.env
* when running this application with the output of `yarn build` * when running this application with the output of `yarn build`
* we will instead load from the global window * we will instead load from the global window
* *
@@ -27,9 +27,9 @@ import type { IFirebaseConfig, ISentryConfig, siteVariants } from './types'
*/ */
const _c = (property: ConfigurationOption, fallbackValue?: string): string => { const _c = (property: ConfigurationOption, fallbackValue?: string): string => {
const configurationSource = ['development', 'test'].includes( const configurationSource = ['development', 'test'].includes(
process.env.NODE_ENV, import.meta.env.NODE_ENV,
) )
? process.env ? import.meta.env
: window?.__OA_COMMUNITY_PLATFORM_CONFIGURATION : window?.__OA_COMMUNITY_PLATFORM_CONFIGURATION
return configurationSource?.[property] || fallbackValue return configurationSource?.[property] || fallbackValue
} }
@@ -62,14 +62,14 @@ const getSiteVariant = (): siteVariants => {
} }
if ( if (
location.host === 'localhost:3456' || location.host === 'localhost:3456' ||
_c('REACT_APP_SITE_VARIANT') === 'test-ci' _c('VITE_APP_SITE_VARIANT') === 'test-ci'
) { ) {
return 'test-ci' return 'test-ci'
} }
if (_c('REACT_APP_SITE_VARIANT') === 'preview') { if (_c('VITE_APP_SITE_VARIANT') === 'preview') {
return 'preview' return 'preview'
} }
switch (_c('REACT_APP_BRANCH')) { switch (_c('VITE_APP_BRANCH')) {
case 'production': case 'production':
return 'production' return 'production'
case 'master': case 'master':
@@ -131,12 +131,12 @@ const firebaseConfigs: { [variant in siteVariants]: IFirebaseConfig } = {
}, },
/** Production/live backend with released frontend */ /** Production/live backend with released frontend */
production: { production: {
apiKey: _c('REACT_APP_FIREBASE_API_KEY'), apiKey: _c('VITE_APP_FIREBASE_API_KEY'),
authDomain: _c('REACT_APP_FIREBASE_AUTH_DOMAIN'), authDomain: _c('VITE_APP_FIREBASE_AUTH_DOMAIN'),
databaseURL: _c('REACT_APP_FIREBASE_DATABASE_URL'), databaseURL: _c('VITE_APP_FIREBASE_DATABASE_URL'),
messagingSenderId: _c('REACT_APP_FIREBASE_MESSAGING_SENDER_ID'), messagingSenderId: _c('VITE_APP_FIREBASE_MESSAGING_SENDER_ID'),
projectId: _c('REACT_APP_FIREBASE_PROJECT_ID'), projectId: _c('VITE_APP_FIREBASE_PROJECT_ID'),
storageBucket: _c('REACT_APP_FIREBASE_STORAGE_BUCKET'), storageBucket: _c('VITE_APP_FIREBASE_STORAGE_BUCKET'),
}, },
} }
/*********************************************************************************************** / /*********************************************************************************************** /
@@ -148,22 +148,22 @@ export const DEV_SITE_ROLE = devSiteRole
export const FIREBASE_CONFIG = firebaseConfigs[siteVariant] export const FIREBASE_CONFIG = firebaseConfigs[siteVariant]
export const SENTRY_CONFIG: ISentryConfig = { export const SENTRY_CONFIG: ISentryConfig = {
dsn: _c( dsn: _c(
'REACT_APP_SENTRY_DSN', 'VITE_APP_SENTRY_DSN',
'https://8c1f7eb4892e48b18956af087bdfa3ac@sentry.io/1399729', 'https://8c1f7eb4892e48b18956af087bdfa3ac@sentry.io/1399729',
), ),
environment: siteVariant, environment: siteVariant,
} }
export const CDN_URL = _c('REACT_APP_CDN_URL', '') export const CDN_URL = _c('VITE_APP_CDN_URL', '')
export const VERSION = _c('REACT_APP_PROJECT_VERSION', '') export const VERSION = _c('VITE_APP_PROJECT_VERSION', '')
export const GA_TRACKING_ID = _c('REACT_APP_GA_TRACKING_ID') export const GA_TRACKING_ID = _c('VITE_APP_GA_TRACKING_ID')
export const PATREON_CLIENT_ID = _c('REACT_APP_PATREON_CLIENT_ID') export const PATREON_CLIENT_ID = _c('VITE_APP_PATREON_CLIENT_ID')
export const API_URL = _c('REACT_APP_API_URL', '') export const API_URL = _c('VITE_APP_API_URL', '')
export const isPreciousPlastic = (): boolean => { export const isPreciousPlastic = (): boolean => {
return ( return (
(_c('REACT_APP_PLATFORM_THEME') || (_c('VITE_APP_PLATFORM_THEME') || localStorage.getItem('platformTheme')) ===
localStorage.getItem('platformTheme')) === 'precious-plastic' 'precious-plastic'
) )
} }

View File

@@ -5,27 +5,27 @@
* type exported from this file * type exported from this file
*/ */
export const _supportedConfigurationOptions = [ export const _supportedConfigurationOptions = [
'REACT_APP_SENTRY_DSN', 'VITE_APP_SENTRY_DSN',
'REACT_APP_PROJECT_VERSION', 'VITE_APP_PROJECT_VERSION',
'REACT_APP_GA_TRACKING_ID', 'VITE_APP_GA_TRACKING_ID',
'REACT_APP_FIREBASE_API_KEY', 'VITE_APP_FIREBASE_API_KEY',
'REACT_APP_FIREBASE_AUTH_DOMAIN', 'VITE_APP_FIREBASE_AUTH_DOMAIN',
'REACT_APP_FIREBASE_DATABASE_URL', 'VITE_APP_FIREBASE_DATABASE_URL',
'REACT_APP_FIREBASE_MESSAGING_SENDER_ID', 'VITE_APP_FIREBASE_MESSAGING_SENDER_ID',
'REACT_APP_FIREBASE_PROJECT_ID', 'VITE_APP_FIREBASE_PROJECT_ID',
'REACT_APP_FIREBASE_STORAGE_BUCKET', 'VITE_APP_FIREBASE_STORAGE_BUCKET',
'REACT_APP_ALGOLIA_PLACES_APP_ID', 'VITE_APP_ALGOLIA_PLACES_APP_ID',
'REACT_APP_ALGOLIA_PLACES_API_KEY', 'VITE_APP_ALGOLIA_PLACES_API_KEY',
'REACT_APP_BRANCH', 'VITE_APP_BRANCH',
'REACT_APP_SITE_VARIANT', 'VITE_APP_SITE_VARIANT',
'REACT_APP_LOG_LEVEL', 'VITE_APP_LOG_LEVEL',
'REACT_APP_LOG_TRANSPORT', 'VITE_APP_LOG_TRANSPORT',
'REACT_APP_SUPPORTED_MODULES', 'VITE_APP_SUPPORTED_MODULES',
'REACT_APP_PLATFORM_THEME', 'VITE_APP_PLATFORM_THEME',
'REACT_APP_PLATFORM_PROFILES', 'VITE_APP_PLATFORM_PROFILES',
'REACT_APP_CDN_URL', 'VITE_APP_CDN_URL',
'REACT_APP_PATREON_CLIENT_ID', 'VITE_APP_PATREON_CLIENT_ID',
'REACT_APP_API_URL', 'VITE_APP_API_URL',
] as const ] as const
export type ConfigurationOption = typeof _supportedConfigurationOptions[number] export type ConfigurationOption = typeof _supportedConfigurationOptions[number]

View File

@@ -1,3 +1,5 @@
import { describe, it } from 'vitest'
import { logger } from './index' import { logger } from './index'
describe('logger', () => { describe('logger', () => {

View File

@@ -2,8 +2,8 @@ import { Logger } from 'tslog'
import { getConfigurationOption } from '../config/config' import { getConfigurationOption } from '../config/config'
const logLevel = getConfigurationOption('REACT_APP_LOG_LEVEL', 'info') const logLevel = getConfigurationOption('VITE_APP_LOG_LEVEL', 'info')
const logTransport = getConfigurationOption('REACT_APP_LOG_TRANSPORT', 'none') const logTransport = getConfigurationOption('VITE_APP_LOG_TRANSPORT', 'none')
const levelNumberToNameMap = { const levelNumberToNameMap = {
silly: 0, silly: 0,

View File

@@ -1,13 +1,15 @@
import { afterAll, describe, expect, it } from 'vitest'
import { getSupportedModules, isModuleSupported, MODULE } from '.' import { getSupportedModules, isModuleSupported, MODULE } from '.'
describe('getSupportedModules', () => { describe('getSupportedModules', () => {
const oldProcessEnv = process.env const defaultModules = import.meta.env.VITE_APP_SUPPORTED_MODULES
afterAll(() => { afterAll(() => {
process.env = oldProcessEnv import.meta.env.VITE_APP_SUPPORTED_MODULES = defaultModules
}) })
it('returns a default set of modules', () => { it('returns a default set of modules', () => {
process.env.REACT_APP_SUPPORTED_MODULES = '' import.meta.env.VITE_APP_SUPPORTED_MODULES = ''
expect(getSupportedModules()).toStrictEqual([ expect(getSupportedModules()).toStrictEqual([
MODULE.CORE, MODULE.CORE,
MODULE.HOWTO, MODULE.HOWTO,
@@ -20,17 +22,17 @@ describe('getSupportedModules', () => {
}) })
it('loads an additional module based on env configuration', () => { it('loads an additional module based on env configuration', () => {
process.env.REACT_APP_SUPPORTED_MODULES = ` ${MODULE.HOWTO} ` import.meta.env.VITE_APP_SUPPORTED_MODULES = ` ${MODULE.HOWTO} `
expect(getSupportedModules()).toStrictEqual([MODULE.CORE, MODULE.HOWTO]) expect(getSupportedModules()).toStrictEqual([MODULE.CORE, MODULE.HOWTO])
}) })
it('loads multiple modules based on env configuration', () => { it('loads multiple modules based on env configuration', () => {
process.env.REACT_APP_SUPPORTED_MODULES = ` ${MODULE.HOWTO} ` import.meta.env.VITE_APP_SUPPORTED_MODULES = ` ${MODULE.HOWTO} `
expect(getSupportedModules()).toStrictEqual([MODULE.CORE, MODULE.HOWTO]) expect(getSupportedModules()).toStrictEqual([MODULE.CORE, MODULE.HOWTO])
}) })
it('ignores a malformed module definitions', () => { it('ignores a malformed module definitions', () => {
process.env.REACT_APP_SUPPORTED_MODULES = `fake module,${MODULE.HOWTO},malicious ` import.meta.env.VITE_APP_SUPPORTED_MODULES = `fake module,${MODULE.HOWTO},malicious `
expect(getSupportedModules()).toStrictEqual([MODULE.CORE, MODULE.HOWTO]) expect(getSupportedModules()).toStrictEqual([MODULE.CORE, MODULE.HOWTO])
}) })
}) })
@@ -41,12 +43,12 @@ describe('isModuleSupported', () => {
}) })
it('returns true for module enabled via env', () => { it('returns true for module enabled via env', () => {
process.env.REACT_APP_SUPPORTED_MODULES = `${MODULE.RESEARCH}` import.meta.env.VITE_APP_SUPPORTED_MODULES = `${MODULE.RESEARCH}`
expect(isModuleSupported(MODULE.RESEARCH)).toBe(true) expect(isModuleSupported(MODULE.RESEARCH)).toBe(true)
}) })
it('returns false for unsupported module', () => { it('returns false for unsupported module', () => {
process.env.REACT_APP_SUPPORTED_MODULES = `${MODULE.HOWTO}` import.meta.env.VITE_APP_SUPPORTED_MODULES = `${MODULE.HOWTO}`
expect(isModuleSupported(MODULE.RESEARCH)).toBe(false) expect(isModuleSupported(MODULE.RESEARCH)).toBe(false)
}) })
}) })

View File

@@ -13,7 +13,7 @@ export enum MODULE {
export const getSupportedModules = (): MODULE[] => { export const getSupportedModules = (): MODULE[] => {
const envModules: string[] = const envModules: string[] =
getConfigurationOption( getConfigurationOption(
'REACT_APP_SUPPORTED_MODULES', 'VITE_APP_SUPPORTED_MODULES',
'howto,map,research,academy,user,question', 'howto,map,research,academy,user,question',
) )
.split(',') .split(',')

View File

@@ -1,9 +1,6 @@
import { getSupportedProfileTypes } from './index' import { describe, expect, it } from 'vitest'
import { SupportedProfileTypesFactory } from './SupportedProfileTypesFactory'
describe('getSupportedProfileTypes', () => { import { SupportedProfileTypesFactory } from './SupportedProfileTypesFactory'
expect(typeof getSupportedProfileTypes).toBe('function')
})
describe('SupportedProfileTypesFactory', () => { describe('SupportedProfileTypesFactory', () => {
it('handles malformed input with default items', () => { it('handles malformed input with default items', () => {
@@ -13,37 +10,41 @@ describe('SupportedProfileTypesFactory', () => {
expect(profiles).toEqual( expect(profiles).toEqual(
expect.arrayContaining([ expect.arrayContaining([
{ {
cleanImageSrc: 'avatar_member_sm.svg', cleanImageSrc:
cleanImageVerifiedSrc: 'avatar_member_sm.svg', '/src/assets/images/themes/precious-plastic/avatar_member_sm.svg',
imageSrc: 'avatar_member_sm.svg', cleanImageVerifiedSrc:
'/src/assets/images/themes/precious-plastic/avatar_member_sm.svg',
imageSrc:
'/src/assets/images/themes/precious-plastic/avatar_member_sm.svg',
label: 'member', label: 'member',
textLabel: 'I am a member', textLabel: 'I am a member',
}, },
{ {
cleanImageSrc: 'map-workspace.svg', cleanImageSrc: '/src/assets/icons/map-workspace.svg',
cleanImageVerifiedSrc: 'map-workspace-verified.svg', cleanImageVerifiedSrc: '/src/assets/icons/map-workspace-verified.svg',
imageSrc: 'pt-workspace.svg', imageSrc: '/src/assets/images/badges/pt-workspace.svg',
label: 'workspace', label: 'workspace',
textLabel: 'I run a workspace', textLabel: 'I run a workspace',
}, },
{ {
cleanImageSrc: 'map-machine.svg', cleanImageSrc: '/src/assets/icons/map-machine.svg',
cleanImageVerifiedSrc: 'map-machine-verified.svg', cleanImageVerifiedSrc: '/src/assets/icons/map-machine-verified.svg',
imageSrc: 'pt-machine-shop.svg', imageSrc: '/src/assets/images/badges/pt-machine-shop.svg',
label: 'machine-builder', label: 'machine-builder',
textLabel: 'I build machines', textLabel: 'I build machines',
}, },
{ {
cleanImageSrc: 'map-community.svg', cleanImageSrc: '/src/assets/icons/map-community.svg',
cleanImageVerifiedSrc: 'map-community-verified.svg', cleanImageVerifiedSrc: '/src/assets/icons/map-community-verified.svg',
imageSrc: 'pt-local-community.svg', imageSrc: '/src/assets/images/badges/pt-local-community.svg',
label: 'community-builder', label: 'community-builder',
textLabel: 'I run a local community', textLabel: 'I run a local community',
}, },
{ {
cleanImageSrc: 'map-collection.svg', cleanImageSrc: '/src/assets/icons/map-collection.svg',
cleanImageVerifiedSrc: 'map-collection-verified.svg', cleanImageVerifiedSrc:
imageSrc: 'pt-collection-point.svg', '/src/assets/icons/map-collection-verified.svg',
imageSrc: '/src/assets/images/badges/pt-collection-point.svg',
label: 'collection-point', label: 'collection-point',
textLabel: 'I collect & sort plastic', textLabel: 'I collect & sort plastic',
}, },
@@ -57,9 +58,12 @@ describe('SupportedProfileTypesFactory', () => {
expect(profiles).toEqual( expect(profiles).toEqual(
expect.arrayContaining([ expect.arrayContaining([
{ {
cleanImageSrc: 'avatar_member_sm.svg', cleanImageSrc:
cleanImageVerifiedSrc: 'avatar_member_sm.svg', '/src/assets/images/themes/precious-plastic/avatar_member_sm.svg',
imageSrc: 'avatar_member_sm.svg', cleanImageVerifiedSrc:
'/src/assets/images/themes/precious-plastic/avatar_member_sm.svg',
imageSrc:
'/src/assets/images/themes/precious-plastic/avatar_member_sm.svg',
label: 'member', label: 'member',
textLabel: 'I am a member', textLabel: 'I am a member',
}, },

View File

@@ -5,7 +5,7 @@ import type { PlatformTheme } from 'oa-themes'
export const getSupportedProfileTypes = (currentTheme?: PlatformTheme) => { export const getSupportedProfileTypes = (currentTheme?: PlatformTheme) => {
const supportedProfileTypes = SupportedProfileTypesFactory( const supportedProfileTypes = SupportedProfileTypesFactory(
getConfigurationOption('REACT_APP_PLATFORM_PROFILES', ''), getConfigurationOption('VITE_APP_PLATFORM_PROFILES', ''),
currentTheme, currentTheme,
)() )()

View File

@@ -1,3 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { getFrameSrc } from './Academy' import { getFrameSrc } from './Academy'
// Mock out the useCommonStores method // Mock out the useCommonStores method
@@ -5,9 +7,9 @@ import { getFrameSrc } from './Academy'
// being instantiated as part of the loading process // being instantiated as part of the loading process
// This is a code smell, which needs to be resolved but // This is a code smell, which needs to be resolved but
// is out of scope for the current task. // is out of scope for the current task.
jest.mock('src/common/hooks/useCommonStores', () => { vi.mock('src/common/hooks/useCommonStores', () => {
return { return {
useCommonStores: jest.fn(), useCommonStores: vi.fn(),
} }
}) })

View File

@@ -1,10 +1,11 @@
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import DonationThankYou from './DonationThankYou' import DonationThankYou from './DonationThankYou'
describe('DonationThankYou', () => { describe('DonationThankYou', () => {
it('sends the expected message', () => { it('sends the expected message', () => {
window.top.postMessage = jest.fn() window.top.postMessage = vi.fn()
render(<DonationThankYou />) render(<DonationThankYou />)

View File

@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import { HowtoProvider } from 'src/test/components' import { HowtoProvider } from 'src/test/components'
import { describe, it } from 'vitest'
import { FormFieldWrapper } from '.' import { FormFieldWrapper } from '.'

View File

@@ -1,3 +1,5 @@
import '@testing-library/jest-dom/vitest'
import { MemoryRouter } from 'react-router-dom' import { MemoryRouter } from 'react-router-dom'
import { ThemeProvider } from '@emotion/react' import { ThemeProvider } from '@emotion/react'
import { act, fireEvent, render } from '@testing-library/react' import { act, fireEvent, render } from '@testing-library/react'
@@ -5,12 +7,13 @@ import { Provider } from 'mobx-react'
import { useCommonStores } from 'src/common/hooks/useCommonStores' import { useCommonStores } from 'src/common/hooks/useCommonStores'
import { FactoryHowto } from 'src/test/factories/Howto' import { FactoryHowto } from 'src/test/factories/Howto'
import { testingThemeStyles } from 'src/test/utils/themeUtils' import { testingThemeStyles } from 'src/test/utils/themeUtils'
import { describe, expect, it, vi } from 'vitest'
import { HowtoForm } from './Howto.form' import { HowtoForm } from './Howto.form'
const Theme = testingThemeStyles const Theme = testingThemeStyles
jest.mock('src/common/hooks/useCommonStores', () => { vi.mock('src/common/hooks/useCommonStores', () => {
return { return {
useCommonStores: () => ({ useCommonStores: () => ({
stores: { stores: {
@@ -26,8 +29,8 @@ jest.mock('src/common/hooks/useCommonStores', () => {
Database: false, Database: false,
Complete: false, Complete: false,
}, },
validateTitleForSlug: jest.fn(), validateTitleForSlug: vi.fn(),
uploadHowTo: jest.fn(), uploadHowTo: vi.fn(),
}, },
tagsStore: { tagsStore: {
allTags: [ allTags: [
@@ -49,8 +52,8 @@ describe('Howto form', () => {
const formValues = FactoryHowto() const formValues = FactoryHowto()
// Act // Act
let wrapper let wrapper
await act(async () => { act(() => {
wrapper = await Wrapper(formValues, 'edit', {}) wrapper = Wrapper(formValues, 'edit', {})
}) })
// Assert // Assert
@@ -156,7 +159,7 @@ describe('Howto form', () => {
}) })
}) })
const Wrapper = async (formValues, parentType, navProps) => { const Wrapper = (formValues, parentType, navProps) => {
return render( return render(
<Provider {...useCommonStores().stores}> <Provider {...useCommonStores().stores}>
<ThemeProvider theme={Theme}> <ThemeProvider theme={Theme}>

View File

@@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react'
import { guidance } from 'src/pages/Howto/labels' import { guidance } from 'src/pages/Howto/labels'
import { HowtoProvider } from 'src/test/components' import { HowtoProvider } from 'src/test/components'
import { FactoryCategory } from 'src/test/factories/Category' import { FactoryCategory } from 'src/test/factories/Category'
import { describe, expect, it } from 'vitest'
import { HowtoCategoryGuidance } from '.' import { HowtoCategoryGuidance } from '.'

View File

@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import { HowtoProvider } from 'src/test/components' import { HowtoProvider } from 'src/test/components'
import { describe, expect, it } from 'vitest'
import { HowtoErrors } from '.' import { HowtoErrors } from '.'

View File

@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import { HowtoProvider } from 'src/test/components' import { HowtoProvider } from 'src/test/components'
import { describe, it } from 'vitest'
import { HowtoFieldDescription } from '.' import { HowtoFieldDescription } from '.'

View File

@@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react'
import { guidance } from 'src/pages/Howto/labels' import { guidance } from 'src/pages/Howto/labels'
import { HowtoProvider } from 'src/test/components' import { HowtoProvider } from 'src/test/components'
import { FactoryCategory } from 'src/test/factories/Category' import { FactoryCategory } from 'src/test/factories/Category'
import { describe, it, vi } from 'vitest'
import { HowtoFieldFiles } from '.' import { HowtoFieldFiles } from '.'
@@ -11,7 +12,7 @@ describe('HowtoFieldFiles', () => {
category: undefined, category: undefined,
fileEditMode: true, fileEditMode: true,
files: [], files: [],
onClick: jest.fn(), onClick: vi.fn(),
showInvalidFileWarning: false, showInvalidFileWarning: false,
} }
@@ -31,7 +32,7 @@ describe('HowtoFieldFiles', () => {
category: FactoryCategory, category: FactoryCategory,
fileEditMode: true, fileEditMode: true,
files: [], files: [],
onClick: jest.fn(), onClick: vi.fn(),
showInvalidFileWarning: false, showInvalidFileWarning: false,
} }

View File

@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import { HowtoProvider } from 'src/test/components' import { HowtoProvider } from 'src/test/components'
import { describe, it } from 'vitest'
import { HowtoFieldStepsContainer } from '.' import { HowtoFieldStepsContainer } from '.'

View File

@@ -1,13 +1,14 @@
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import { HowtoProvider } from 'src/test/components' import { HowtoProvider } from 'src/test/components'
import { describe, it, vi } from 'vitest'
import { HowtoFieldTitle } from '.' import { HowtoFieldTitle } from '.'
import type { HowtoStore } from 'src/stores/Howto/howto.store' import type { HowtoStore } from 'src/stores/Howto/howto.store'
import type { ParentType } from './Howto.form' import type { ParentType } from './Howto.form'
jest.mock('src/stores/Howto/howto.store') vi.mock('src/stores/Howto/howto.store')
const store = jest.createMockFromModule('src/stores/Howto/howto.store') const store = await vi.importMock('src/stores/Howto/howto.store')
describe('HowtoFieldTitle', () => { describe('HowtoFieldTitle', () => {
it('renders', async () => { it('renders', async () => {

View File

@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import { HowtoProvider } from 'src/test/components' import { HowtoProvider } from 'src/test/components'
import { describe, it, vi } from 'vitest'
import { HowtoFieldStep } from '.' import { HowtoFieldStep } from '.'
@@ -9,8 +10,8 @@ describe('HowtoFieldStep', () => {
step: [], step: [],
index: 0, index: 0,
images: [], images: [],
onDelete: jest.fn(() => null), onDelete: vi.fn(() => null),
moveStep: jest.fn(() => null), moveStep: vi.fn(() => null),
} }
render( render(

View File

@@ -1,4 +1,5 @@
import { intro, steps } from 'src/pages/Howto/labels' import { intro, steps } from 'src/pages/Howto/labels'
import { describe, expect, it } from 'vitest'
import { transformHowtoErrors } from '.' import { transformHowtoErrors } from '.'

View File

@@ -1,3 +1,5 @@
import '@testing-library/jest-dom/vitest'
import { import {
createMemoryRouter, createMemoryRouter,
createRoutesFromElements, createRoutesFromElements,
@@ -10,6 +12,7 @@ import { act, render, within } from '@testing-library/react'
import { Provider } from 'mobx-react' import { Provider } from 'mobx-react'
import { preciousPlasticTheme } from 'oa-themes' import { preciousPlasticTheme } from 'oa-themes'
import { FactoryHowto, FactoryHowtoStep } from 'src/test/factories/Howto' import { FactoryHowto, FactoryHowtoStep } from 'src/test/factories/Howto'
import { describe, expect, it, vi } from 'vitest'
import type { HowtoStore } from 'src/stores/Howto/howto.store' import type { HowtoStore } from 'src/stores/Howto/howto.store'
@@ -18,21 +21,21 @@ const Theme = preciousPlasticTheme.styles
const howto = FactoryHowto() const howto = FactoryHowto()
const mockHowtoStore = () => ({ const mockHowtoStore = () => ({
setActiveHowtoBySlug: jest.fn(), setActiveHowtoBySlug: vi.fn(),
activeHowto: howto, activeHowto: howto,
needsModeration: jest.fn().mockReturnValue(false), needsModeration: vi.fn().mockReturnValue(false),
incrementViewCount: jest.fn(), incrementViewCount: vi.fn(),
removeActiveHowto: jest.fn(), removeActiveHowto: vi.fn(),
}) })
jest.mock('src/common/hooks/useCommonStores', () => ({ vi.mock('src/common/hooks/useCommonStores', () => ({
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
__esModule: true, __esModule: true,
useCommonStores: () => ({ useCommonStores: () => ({
stores: { stores: {
userStore: {}, userStore: {},
aggregationsStore: { aggregationsStore: {
isVerified: jest.fn((userId) => userId === 'HowtoAuthor'), isVerified: vi.fn((userId) => userId === 'HowtoAuthor'),
users_verified: { users_verified: {
HowtoAuthor: true, HowtoAuthor: true,
}, },

View File

@@ -1,40 +1,42 @@
import '@testing-library/jest-dom' import '@testing-library/jest-dom'
import { UserRole } from 'oa-shared/models' import { UserRole } from 'oa-shared'
import { FactoryUser } from 'src/test/factories/User' import { FactoryUser } from 'src/test/factories/User'
import { describe, expect, it, vi } from 'vitest'
import { exportedForTesting } from './howto.service' import { exportedForTesting } from './howto.service'
const mockWhere = jest.fn() const mockWhere = vi.fn()
const mockOrderBy = jest.fn() const mockOrderBy = vi.fn()
const mockLimit = jest.fn() const mockLimit = vi.fn()
const mockOr = jest.fn() const mockOr = vi.fn()
jest.mock('firebase/firestore', () => ({ vi.mock('firebase/firestore', () => ({
collection: jest.fn(), collection: vi.fn(),
query: jest.fn(), query: vi.fn(),
and: jest.fn(), and: vi.fn(),
where: (path, op, value) => mockWhere(path, op, value), where: (path, op, value) => mockWhere(path, op, value),
limit: (limit) => mockLimit(limit), limit: (limit) => mockLimit(limit),
orderBy: (field, direction) => mockOrderBy(field, direction), orderBy: (field, direction) => mockOrderBy(field, direction),
or: (constraints) => mockOr(constraints), or: (constraints) => mockOr(constraints),
})) }))
jest.mock('../../stores/databaseV2/endpoints', () => ({ vi.mock('../../stores/databaseV2/endpoints', () => ({
DB_ENDPOINTS: { DB_ENDPOINTS: {
howtos: 'howtos', howtos: 'howtos',
categories: 'categories', categories: 'categories',
}, },
})) }))
jest.mock('../../config/config', () => ({ vi.mock('../../config/config', () => ({
getConfigurationOption: jest.fn(), getConfigurationOption: vi.fn(),
FIREBASE_CONFIG: { FIREBASE_CONFIG: {
apiKey: 'AIyChVN', apiKey: 'AIyChVN',
databaseURL: 'https://test.firebaseio.com', databaseURL: 'https://test.firebaseio.com',
projectId: 'test', projectId: 'test',
storageBucket: 'test.appspot.com', storageBucket: 'test.appspot.com',
}, },
localStorage: jest.fn(), localStorage: vi.fn(),
SITE: 'unit-tests',
})) }))
describe('howtos.search', () => { describe('howtos.search', () => {

View File

@@ -1,3 +1,5 @@
import '@testing-library/jest-dom/vitest'
import { import {
createMemoryRouter, createMemoryRouter,
createRoutesFromElements, createRoutesFromElements,
@@ -12,6 +14,7 @@ import { useCommonStores } from 'src/common/hooks/useCommonStores'
import { FactoryMapPin } from 'src/test/factories/MapPin' import { FactoryMapPin } from 'src/test/factories/MapPin'
import { FactoryUser } from 'src/test/factories/User' import { FactoryUser } from 'src/test/factories/User'
import { testingThemeStyles } from 'src/test/utils/themeUtils' import { testingThemeStyles } from 'src/test/utils/themeUtils'
import { describe, expect, it, vi } from 'vitest'
import { MapPinServiceContext } from './map.service' import { MapPinServiceContext } from './map.service'
import Maps from './Maps' import Maps from './Maps'
@@ -20,17 +23,17 @@ import type { IMapPinService } from './map.service'
const Theme = testingThemeStyles const Theme = testingThemeStyles
jest.mock('src/common/hooks/useCommonStores', () => ({ vi.mock('src/common/hooks/useCommonStores', () => ({
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
__esModule: true, __esModule: true,
useCommonStores: () => ({ useCommonStores: () => ({
stores: { stores: {
userStore: { userStore: {
getUserProfile: jest.fn(), getUserProfile: vi.fn(),
updateUserBadge: jest.fn(), updateUserBadge: vi.fn(),
}, },
aggregationsStore: { aggregationsStore: {
isVerified: jest.fn(), isVerified: vi.fn(),
users_verified: { users_verified: {
HowtoAuthor: true, HowtoAuthor: true,
}, },
@@ -52,7 +55,7 @@ describe('Maps', () => {
Object.defineProperty(global.navigator, 'geolocation', { Object.defineProperty(global.navigator, 'geolocation', {
writable: true, writable: true,
value: { value: {
getCurrentPosition: jest.fn(), getCurrentPosition: vi.fn(),
}, },
}) })
@@ -86,7 +89,7 @@ const Wrapper = async (path = '/map') => {
) )
const mockMapPinService: IMapPinService = { const mockMapPinService: IMapPinService = {
getMapPinByUserId: jest.fn().mockResolvedValue({ getMapPinByUserId: vi.fn().mockResolvedValue({
...FactoryUser({ ...FactoryUser({
moderation: IModerationStatus.ACCEPTED, moderation: IModerationStatus.ACCEPTED,
}), }),
@@ -95,8 +98,8 @@ const Wrapper = async (path = '/map') => {
shortDescription: 'description', shortDescription: 'description',
}, },
}), }),
getMapPinSelf: jest.fn().mockResolvedValue({}), getMapPinSelf: vi.fn().mockResolvedValue({}),
getMapPins: jest.fn().mockImplementation(() => { getMapPins: vi.fn().mockImplementation(() => {
return Promise.resolve([]) return Promise.resolve([])
}), }),
} }

View File

@@ -1,19 +1,20 @@
import { faker } from '@faker-js/faker' import { faker } from '@faker-js/faker'
import { DB_ENDPOINTS } from 'src/models' import { DB_ENDPOINTS } from 'src/models'
import { describe, expect, it, vi } from 'vitest'
import { mapPinService } from './map.service' import { mapPinService } from './map.service'
const mockWhere = jest.fn() const mockWhere = vi.fn()
const mockLimit = jest.fn() const mockLimit = vi.fn()
const mockQuery = jest.fn() const mockQuery = vi.fn()
const mockCollection = jest.fn() const mockCollection = vi.fn()
const mockQuerySnapshot = { const mockQuerySnapshot = {
docs: [{ data: () => {} }], docs: [{ data: () => {} }],
} }
const mockGetDocs = jest.fn().mockResolvedValue(mockQuerySnapshot) const mockGetDocs = vi.fn().mockResolvedValue(mockQuerySnapshot)
jest.mock('firebase/firestore', () => ({ vi.mock('firebase/firestore', () => ({
collection: (_firebase, connectionName) => mockCollection(connectionName), collection: (_firebase, connectionName) => mockCollection(connectionName),
query: (collectionRef, whereResult) => mockQuery(collectionRef, whereResult), query: (collectionRef, whereResult) => mockQuery(collectionRef, whereResult),
where: (path, op, value) => mockWhere(path, op, value), where: (path, op, value) => mockWhere(path, op, value),
@@ -25,7 +26,7 @@ describe('map.service', () => {
describe('getMapPins', () => { describe('getMapPins', () => {
it('fetches map pins', async () => { it('fetches map pins', async () => {
// prepare // prepare
global.fetch = jest.fn().mockResolvedValue({ global.fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve([{ _id: '1' }]), json: () => Promise.resolve([{ _id: '1' }]),
}) })
@@ -38,7 +39,7 @@ describe('map.service', () => {
it('handles errors', async () => { it('handles errors', async () => {
// prepare // prepare
global.fetch = jest.fn().mockRejectedValue('error') global.fetch = vi.fn().mockRejectedValue('error')
// act // act
const result = await mapPinService.getMapPins() const result = await mapPinService.getMapPins()
@@ -51,7 +52,7 @@ describe('map.service', () => {
describe('getMapPinByUserId', () => { describe('getMapPinByUserId', () => {
it('fetches map pin by user id', async () => { it('fetches map pin by user id', async () => {
// prepare // prepare
global.fetch = jest.fn().mockResolvedValue({ global.fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve({ _id: '1' }), json: () => Promise.resolve({ _id: '1' }),
}) })
@@ -64,7 +65,7 @@ describe('map.service', () => {
it('handles errors', async () => { it('handles errors', async () => {
// prepare // prepare
global.fetch = jest.fn().mockRejectedValue('error') global.fetch = vi.fn().mockRejectedValue('error')
// act // act
const result = await mapPinService.getMapPinByUserId('1') const result = await mapPinService.getMapPinByUserId('1')

View File

@@ -16,6 +16,10 @@ export const QuestionCategoryField = () => {
useEffect(() => { useEffect(() => {
const initCategories = async () => { const initCategories = async () => {
const categories = await questionService.getQuestionCategories() const categories = await questionService.getQuestionCategories()
if (!categories) {
return
}
const selectOptions = categories.map((category) => ({ const selectOptions = categories.map((category) => ({
value: category, value: category,
label: category.label, label: category.label,

View File

@@ -62,16 +62,18 @@ export const QuestionListing = () => {
ITEMS_PER_PAGE, ITEMS_PER_PAGE,
) )
if (skipFrom) { if (result) {
// if skipFrom is set, means we are requesting another page that should be appended if (skipFrom) {
setQuestions((questions) => [...questions, ...result.items]) // if skipFrom is set, means we are requesting another page that should be appended
} else { setQuestions((questions) => [...questions, ...result.items])
setQuestions(result.items) } else {
setQuestions(result.items)
}
setLastVisible(result.lastVisible)
setTotal(result.total)
} }
setLastVisible(result.lastVisible)
setTotal(result.total)
} catch (error) { } catch (error) {
logger.error('error fetching questions', error) logger.error('error fetching questions', error)
} }

View File

@@ -1,3 +1,5 @@
import '@testing-library/jest-dom/vitest'
import { import {
createMemoryRouter, createMemoryRouter,
createRoutesFromElements, createRoutesFromElements,
@@ -14,9 +16,12 @@ import { FactoryDiscussion } from 'src/test/factories/Discussion'
import { FactoryQuestionItem } from 'src/test/factories/Question' import { FactoryQuestionItem } from 'src/test/factories/Question'
import { FactoryUser } from 'src/test/factories/User' import { FactoryUser } from 'src/test/factories/User'
import { testingThemeStyles } from 'src/test/utils/themeUtils' import { testingThemeStyles } from 'src/test/utils/themeUtils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { QuestionPage } from './QuestionPage' import { QuestionPage } from './QuestionPage'
import type { Mock } from 'vitest'
const Theme = testingThemeStyles const Theme = testingThemeStyles
const activeUser = FactoryUser({ const activeUser = FactoryUser({
@@ -29,21 +34,21 @@ const mockQuestionItem = FactoryQuestionItem({
}) })
const mockDiscussionItem = FactoryDiscussion() const mockDiscussionItem = FactoryDiscussion()
jest.mock('src/common/hooks/useCommonStores', () => ({ vi.mock('src/common/hooks/useCommonStores', () => ({
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
__esModule: true, __esModule: true,
useCommonStores: () => ({ useCommonStores: () => ({
stores: { stores: {
userStore: { userStore: {
getUserByUsername: jest.fn().mockResolvedValue(mockUser), getUserByUsername: vi.fn().mockResolvedValue(mockUser),
}, },
aggregationsStore: { aggregationsStore: {
isVerified: jest.fn(), isVerified: vi.fn(),
users_verified: {}, users_verified: {},
}, },
tagsStore: {}, tagsStore: {},
discussionStore: { discussionStore: {
fetchOrCreateDiscussionBySource: jest.fn().mockResolvedValue({ fetchOrCreateDiscussionBySource: vi.fn().mockResolvedValue({
mockDiscussionItem, mockDiscussionItem,
}), }),
activeUser: mockUser, activeUser: mockUser,
@@ -52,8 +57,8 @@ jest.mock('src/common/hooks/useCommonStores', () => ({
}), }),
})) }))
jest.mock('src/stores/Question/question.store') vi.mock('src/stores/Question/question.store')
jest.mock('src/stores/Discussions/discussions.store') vi.mock('src/stores/Discussions/discussions.store')
describe('Questions', () => { describe('Questions', () => {
let mockQuestionStore let mockQuestionStore
@@ -62,20 +67,20 @@ describe('Questions', () => {
// Setup a fresh instance of the mock store before each test // Setup a fresh instance of the mock store before each test
mockQuestionStore = { mockQuestionStore = {
activeQuestionItem: mockQuestionItem, activeQuestionItem: mockQuestionItem,
incrementViewCount: jest.fn(), incrementViewCount: vi.fn(),
fetchQuestionBySlug: jest.fn(() => { fetchQuestionBySlug: vi.fn(() => {
return mockQuestionItem return mockQuestionItem
}), }),
upsertQuestion: jest.fn(), upsertQuestion: vi.fn(),
toggleSubscriberStatusByUserName: jest.fn(), toggleSubscriberStatusByUserName: vi.fn(),
toggleUsefulByUser: jest.fn(), toggleUsefulByUser: vi.fn(),
} }
;(useQuestionStore as jest.Mock).mockReturnValue(mockQuestionStore) ;(useQuestionStore as Mock).mockReturnValue(mockQuestionStore)
}) })
afterEach(() => { afterEach(() => {
// Clear all mocks after each test to ensure there's no leakage between tests // Clear all mocks after each test to ensure there's no leakage between tests
jest.clearAllMocks() vi.clearAllMocks()
}) })
describe('Breadcrumbs', () => { describe('Breadcrumbs', () => {
@@ -91,10 +96,10 @@ describe('Questions', () => {
_deleted: faker.datatype.boolean(), _deleted: faker.datatype.boolean(),
_contentModifiedTimestamp: faker.date.past().toString(), _contentModifiedTimestamp: faker.date.past().toString(),
} }
;(useQuestionStore as jest.Mock).mockReturnValue({ ;(useQuestionStore as Mock).mockReturnValue({
...mockQuestionStore, ...mockQuestionStore,
activeQuestionItem: mockQuestionItem, activeQuestionItem: mockQuestionItem,
fetchQuestionBySlug: jest.fn(() => { fetchQuestionBySlug: vi.fn(() => {
return mockQuestionItem return mockQuestionItem
}), }),
}) })
@@ -130,10 +135,10 @@ describe('Questions', () => {
mockQuestionItem.title = mockQuestionItem.title =
'Do you prefer camping near a lake or in a forest?' 'Do you prefer camping near a lake or in a forest?'
mockQuestionItem.questionCategory = undefined mockQuestionItem.questionCategory = undefined
;(useQuestionStore as jest.Mock).mockReturnValue({ ;(useQuestionStore as Mock).mockReturnValue({
...mockQuestionStore, ...mockQuestionStore,
activeQuestionItem: mockQuestionItem, activeQuestionItem: mockQuestionItem,
fetchQuestionBySlug: jest.fn(() => { fetchQuestionBySlug: vi.fn(() => {
return mockQuestionItem return mockQuestionItem
}), }),
}) })

View File

@@ -1,8 +1,4 @@
/* eslint-disable max-classes-per-file */ import '@testing-library/jest-dom/vitest'
jest.mock('../../stores/common/module.store')
jest.mock('src/utils/validators')
import '@testing-library/jest-dom'
import { import {
createMemoryRouter, createMemoryRouter,
@@ -22,10 +18,15 @@ import { FactoryDiscussion } from 'src/test/factories/Discussion'
import { FactoryQuestionItem } from 'src/test/factories/Question' import { FactoryQuestionItem } from 'src/test/factories/Question'
import { FactoryUser } from 'src/test/factories/User' import { FactoryUser } from 'src/test/factories/User'
import { testingThemeStyles } from 'src/test/utils/themeUtils' import { testingThemeStyles } from 'src/test/utils/themeUtils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { questionRouteElements } from './question.routes' import { questionRouteElements } from './question.routes'
import type { QuestionStore } from 'src/stores/Question/question.store' import type { QuestionStore } from 'src/stores/Question/question.store'
import type { Mock } from 'vitest'
vi.mock('../../stores/common/module.store')
vi.mock('src/utils/validators')
const Theme = testingThemeStyles const Theme = testingThemeStyles
let mockActiveUser = FactoryUser() let mockActiveUser = FactoryUser()
@@ -33,7 +34,7 @@ const mockDiscussionItem = FactoryDiscussion()
// Similar to issues in Academy.test.tsx - stub methods called in user store constructor // Similar to issues in Academy.test.tsx - stub methods called in user store constructor
// TODO - replace with mock store or avoid direct call // TODO - replace with mock store or avoid direct call
jest.mock('src/common/hooks/useCommonStores', () => ({ vi.mock('src/common/hooks/useCommonStores', () => ({
__esModule: true, __esModule: true,
useCommonStores: () => ({ useCommonStores: () => ({
stores: { stores: {
@@ -41,7 +42,7 @@ jest.mock('src/common/hooks/useCommonStores', () => ({
user: mockActiveUser, user: mockActiveUser,
}, },
aggregationsStore: { aggregationsStore: {
isVerified: jest.fn(), isVerified: vi.fn(),
users_verified: { users_verified: {
HowtoAuthor: true, HowtoAuthor: true,
}, },
@@ -59,50 +60,50 @@ jest.mock('src/common/hooks/useCommonStores', () => ({
allQuestionCategories: [], allQuestionCategories: [],
}, },
discussionStore: { discussionStore: {
fetchOrCreateDiscussionBySource: jest.fn().mockResolvedValue({ fetchOrCreateDiscussionBySource: vi.fn().mockResolvedValue({
mockDiscussionItem, mockDiscussionItem,
}), }),
activeUser: jest.fn().mockResolvedValue(mockActiveUser), activeUser: vi.fn().mockResolvedValue(mockActiveUser),
}, },
}, },
}), }),
})) }))
const mockedUsedNavigate = jest.fn() const mockedUsedNavigate = vi.fn()
jest.mock('react-router-dom', () => ({ vi.mock('react-router-dom', async () => ({
...(jest.requireActual('react-router-dom') as any), ...((await vi.importActual('react-router-dom')) as any),
useNavigate: () => mockedUsedNavigate, useNavigate: () => mockedUsedNavigate,
})) }))
class mockQuestionStoreClass implements Partial<QuestionStore> { class mockQuestionStoreClass implements Partial<QuestionStore> {
setActiveQuestionItemBySlug = jest.fn() setActiveQuestionItemBySlug = vi.fn()
needsModeration = jest.fn().mockResolvedValue(true) needsModeration = vi.fn().mockResolvedValue(true)
incrementViewCount = jest.fn() incrementViewCount = vi.fn()
activeQuestionItem = FactoryQuestionItem({ activeQuestionItem = FactoryQuestionItem({
title: 'Question article title', title: 'Question article title',
}) })
QuestionUploadStatus = {} as any QuestionUploadStatus = {} as any
updateUploadStatus = {} as any updateUploadStatus = {} as any
formatQuestionCommentList = jest.fn() formatQuestionCommentList = vi.fn()
getActiveQuestionUpdateComments = jest.fn() getActiveQuestionUpdateComments = vi.fn()
lockQuestionItem = jest.fn() lockQuestionItem = vi.fn()
lockQuestionUpdate = jest.fn() lockQuestionUpdate = vi.fn()
unlockQuestionUpdate = jest.fn() unlockQuestionUpdate = vi.fn()
upsertQuestion = jest.fn() upsertQuestion = vi.fn()
fetchQuestions = jest.fn().mockResolvedValue([]) fetchQuestions = vi.fn().mockResolvedValue([])
fetchQuestionBySlug = jest.fn() fetchQuestionBySlug = vi.fn()
votedUsefulCount = 0 votedUsefulCount = 0
subscriberCount = 0 subscriberCount = 0
userCanEditQuestion = true userCanEditQuestion = true
} }
const mockQuestionService: typeof questionService = { const mockQuestionService: typeof questionService = {
getQuestionCategories: jest.fn(() => { getQuestionCategories: vi.fn(() => {
return new Promise((resolve) => { return new Promise((resolve) => {
resolve([]) resolve([])
}) })
}), }),
search: jest.fn(() => { search: vi.fn(() => {
return new Promise((resolve) => { return new Promise((resolve) => {
resolve({ items: [], total: 0, lastVisible: undefined }) resolve({ items: [], total: 0, lastVisible: undefined })
}) })
@@ -110,25 +111,25 @@ const mockQuestionService: typeof questionService = {
} }
const mockQuestionStore = new mockQuestionStoreClass() const mockQuestionStore = new mockQuestionStoreClass()
jest.mock('src/stores/Question/question.store') vi.mock('src/stores/Question/question.store')
jest.mock('src/stores/Discussions/discussions.store') vi.mock('src/stores/Discussions/discussions.store')
jest.mock('src/pages/Question/question.service') vi.mock('src/pages/Question/question.service')
describe('question.routes', () => { describe('question.routes', () => {
beforeEach(() => { beforeEach(() => {
;(useQuestionStore as jest.Mock).mockReturnValue(mockQuestionStore) ;(useQuestionStore as Mock).mockReturnValue(mockQuestionStore)
questionService.getQuestionCategories = jest.fn().mockResolvedValue([]) questionService.getQuestionCategories = vi.fn().mockResolvedValue([])
}) })
afterEach(() => { afterEach(() => {
jest.restoreAllMocks() vi.restoreAllMocks()
cleanup() cleanup()
}) })
describe('/questions/', () => { describe('/questions/', () => {
it('renders a loading state', async () => { it('renders a loading state', async () => {
let wrapper let wrapper
mockQuestionService.search = jest.fn(() => { mockQuestionService.search = vi.fn(() => {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout( setTimeout(
() => resolve({ items: [], total: 0, lastVisible: undefined }), () => resolve({ items: [], total: 0, lastVisible: undefined }),
@@ -169,7 +170,7 @@ describe('question.routes', () => {
const questionTitle = faker.lorem.words(3) const questionTitle = faker.lorem.words(3)
const questionSlug = faker.lorem.slug() const questionSlug = faker.lorem.slug()
questionService.search = jest.fn(() => { questionService.search = vi.fn(() => {
return new Promise((resolve) => { return new Promise((resolve) => {
resolve({ resolve({
items: [ items: [
@@ -205,7 +206,7 @@ describe('question.routes', () => {
it('allows user to create a question', async () => { it('allows user to create a question', async () => {
let wrapper let wrapper
// Arrange // Arrange
const mockUpsertQuestion = jest.fn().mockResolvedValue({ const mockUpsertQuestion = vi.fn().mockResolvedValue({
slug: 'question-title', slug: 'question-title',
}) })
useQuestionStore.mockReturnValue({ useQuestionStore.mockReturnValue({
@@ -249,8 +250,8 @@ describe('question.routes', () => {
let wrapper let wrapper
const question = FactoryQuestionItem() const question = FactoryQuestionItem()
const activeUser = FactoryUser({}) const activeUser = FactoryUser({})
const mockFetchQuestionBySlug = jest.fn().mockResolvedValue(question) const mockFetchQuestionBySlug = vi.fn().mockResolvedValue(question)
const mockIncrementViewCount = jest.fn() const mockIncrementViewCount = vi.fn()
useQuestionStore.mockReturnValue({ useQuestionStore.mockReturnValue({
...mockQuestionStore, ...mockQuestionStore,
activeUser, activeUser,
@@ -292,7 +293,7 @@ describe('question.routes', () => {
const question = FactoryQuestionItem({ const question = FactoryQuestionItem({
subscribers: [user.userName], subscribers: [user.userName],
}) })
const mockFetchQuestionBySlug = jest.fn().mockResolvedValue(question) const mockFetchQuestionBySlug = vi.fn().mockResolvedValue(question)
useQuestionStore.mockReturnValue({ useQuestionStore.mockReturnValue({
...mockQuestionStore, ...mockQuestionStore,
activeUser: user, activeUser: user,
@@ -312,7 +313,7 @@ describe('question.routes', () => {
it('supports follow behaviour', async () => { it('supports follow behaviour', async () => {
let wrapper let wrapper
const question = FactoryQuestionItem() const question = FactoryQuestionItem()
const mockFetchQuestionBySlug = jest.fn().mockResolvedValue(question) const mockFetchQuestionBySlug = vi.fn().mockResolvedValue(question)
useQuestionStore.mockReturnValue({ useQuestionStore.mockReturnValue({
...mockQuestionStore, ...mockQuestionStore,
fetchQuestionBySlug: mockFetchQuestionBySlug, fetchQuestionBySlug: mockFetchQuestionBySlug,
@@ -330,7 +331,7 @@ describe('question.routes', () => {
let wrapper let wrapper
mockActiveUser = FactoryUser() mockActiveUser = FactoryUser()
const question = FactoryQuestionItem() const question = FactoryQuestionItem()
const mockFetchQuestionBySlug = jest.fn().mockResolvedValue(question) const mockFetchQuestionBySlug = vi.fn().mockResolvedValue(question)
useQuestionStore.mockReturnValue({ useQuestionStore.mockReturnValue({
...mockQuestionStore, ...mockQuestionStore,
fetchQuestionBySlug: mockFetchQuestionBySlug, fetchQuestionBySlug: mockFetchQuestionBySlug,
@@ -356,7 +357,7 @@ describe('question.routes', () => {
_createdBy: mockActiveUser.userName, _createdBy: mockActiveUser.userName,
}) })
const mockFetchQuestionBySlug = jest.fn().mockResolvedValue(question) const mockFetchQuestionBySlug = vi.fn().mockResolvedValue(question)
useQuestionStore.mockReturnValue({ useQuestionStore.mockReturnValue({
...mockQuestionStore, ...mockQuestionStore,
@@ -402,13 +403,13 @@ describe('question.routes', () => {
title: faker.lorem.words(1), title: faker.lorem.words(1),
_createdBy: 'author', _createdBy: 'author',
}) })
const mockUpsertQuestion = jest.fn().mockResolvedValue({ const mockUpsertQuestion = vi.fn().mockResolvedValue({
slug: 'question-title', slug: 'question-title',
}) })
useQuestionStore.mockReturnValue({ useQuestionStore.mockReturnValue({
...mockQuestionStore, ...mockQuestionStore,
fetchQuestionBySlug: jest.fn().mockResolvedValue(questionItem), fetchQuestionBySlug: vi.fn().mockResolvedValue(questionItem),
upsertQuestion: mockUpsertQuestion, upsertQuestion: mockUpsertQuestion,
activeUser: mockActiveUser, activeUser: mockActiveUser,
}) })
@@ -456,7 +457,7 @@ describe('question.routes', () => {
useQuestionStore.mockReturnValue({ useQuestionStore.mockReturnValue({
...mockQuestionStore, ...mockQuestionStore,
fetchQuestionBySlug: jest.fn().mockResolvedValue( fetchQuestionBySlug: vi.fn().mockResolvedValue(
FactoryQuestionItem({ FactoryQuestionItem({
slug: 'slug', slug: 'slug',
_createdBy: 'author', _createdBy: 'author',

View File

@@ -1,35 +1,38 @@
import '@testing-library/jest-dom' import '@testing-library/jest-dom'
import { describe, expect, it, vi } from 'vitest'
import { exportedForTesting } from './question.service' import { exportedForTesting } from './question.service'
const mockWhere = jest.fn() const mockWhere = vi.fn()
const mockOrderBy = jest.fn() const mockOrderBy = vi.fn()
const mockLimit = jest.fn() const mockLimit = vi.fn()
jest.mock('firebase/firestore', () => ({ vi.mock('firebase/firestore', () => ({
collection: jest.fn(), collection: vi.fn(),
query: jest.fn(), query: vi.fn(),
and: jest.fn(), and: vi.fn(),
where: (path, op, value) => mockWhere(path, op, value), where: (path, op, value) => mockWhere(path, op, value),
limit: (limit) => mockLimit(limit), limit: (limit) => mockLimit(limit),
orderBy: (field, direction) => mockOrderBy(field, direction), orderBy: (field, direction) => mockOrderBy(field, direction),
})) }))
jest.mock('../../stores/databaseV2/endpoints', () => ({ vi.mock('../../stores/databaseV2/endpoints', () => ({
DB_ENDPOINTS: { DB_ENDPOINTS: {
questions: 'questions', questions: 'questions',
questionCategories: 'questionCategories', questionCategories: 'questionCategories',
}, },
})) }))
jest.mock('../../config/config', () => ({ vi.mock('../../config/config', () => ({
getConfigurationOption: jest.fn(), getConfigurationOption: vi.fn(),
FIREBASE_CONFIG: { FIREBASE_CONFIG: {
apiKey: 'AIyChVN', apiKey: 'AIyChVN',
databaseURL: 'https://test.firebaseio.com', databaseURL: 'https://test.firebaseio.com',
projectId: 'test', projectId: 'test',
storageBucket: 'test.appspot.com', storageBucket: 'test.appspot.com',
}, },
localStorage: jest.fn(), localStorage: vi.fn(),
SITE: 'unit-tests',
})) }))
describe('question.search', () => { describe('question.search', () => {

View File

@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import { ResearchProvider } from 'src/test/components' import { ResearchProvider } from 'src/test/components'
import { describe, expect, it } from 'vitest'
import { ResearchErrors } from '.' import { ResearchErrors } from '.'

View File

@@ -1,15 +1,17 @@
import { act } from 'react-dom/test-utils' import '@testing-library/jest-dom/vitest'
import { MemoryRouter } from 'react-router-dom' import { MemoryRouter } from 'react-router-dom'
import { ThemeProvider } from '@emotion/react' import { ThemeProvider } from '@emotion/react'
import { fireEvent, render } from '@testing-library/react' import { render } from '@testing-library/react'
import { FactoryResearchItemUpdate } from 'src/test/factories/ResearchItem' import { FactoryResearchItemUpdate } from 'src/test/factories/ResearchItem'
import { testingThemeStyles } from 'src/test/utils/themeUtils' import { testingThemeStyles } from 'src/test/utils/themeUtils'
import { describe, expect, it, vi } from 'vitest'
import { ResearchUpdateForm } from './ResearchUpdate.form' import { ResearchUpdateForm } from './ResearchUpdate.form'
const Theme = testingThemeStyles const Theme = testingThemeStyles
jest.mock('src/stores/Research/research.store', () => { vi.mock('src/stores/Research/research.store', () => {
return { return {
useResearchStore: () => ({ useResearchStore: () => ({
updateUploadStatus: { updateUploadStatus: {
@@ -19,8 +21,8 @@ jest.mock('src/stores/Research/research.store', () => {
Database: false, Database: false,
Complete: false, Complete: false,
}, },
isTitleThatReusesSlug: jest.fn(), isTitleThatReusesSlug: vi.fn(),
unlockResearchUpdate: jest.fn(), unlockResearchUpdate: vi.fn(),
}), }),
} }
}) })
@@ -32,10 +34,7 @@ describe('Research update form', () => {
fileLink: 'www.filedonwload.test', fileLink: 'www.filedonwload.test',
}) })
let wrapper const wrapper = getWrapper(formValues, 'create', {})
await act(async () => {
wrapper = await getWrapper(formValues, 'create', {})
})
expect( expect(
wrapper.queryByTestId('invalid-file-warning'), wrapper.queryByTestId('invalid-file-warning'),
@@ -49,41 +48,17 @@ describe('Research update form', () => {
}) })
// Act // Act
let wrapper const wrapper = getWrapper(formValues, 'create', {})
await act(async () => {
wrapper = await getWrapper(formValues, 'create', {})
})
// Assert // Assert
expect( expect(
wrapper.queryByTestId('invalid-file-warning'), wrapper.queryByTestId('invalid-file-warning'),
).not.toBeInTheDocument() ).not.toBeInTheDocument()
}) })
it('Appears when submitting 2 file types', async () => {
// Arrange
const formValues = FactoryResearchItemUpdate({
images: [new File(['hello'], 'hello.png')],
files: [new File(['test file content'], 'test-file.zip')],
fileLink: 'www.filedownload.test',
})
// Act
let wrapper
await act(async () => {
wrapper = await getWrapper(formValues, 'create', {})
// submit form
const submitFormButton = wrapper.getByTestId('submit-form')
fireEvent.click(submitFormButton)
})
// Assert
expect(wrapper.queryByTestId('invalid-file-warning')).toBeInTheDocument()
})
}) })
}) })
const getWrapper = async (formValues, parentType, navProps) => { const getWrapper = (formValues, parentType, navProps) => {
return render( return render(
<ThemeProvider theme={Theme}> <ThemeProvider theme={Theme}>
<MemoryRouter initialEntries={['/research/:slug/update']}> <MemoryRouter initialEntries={['/research/:slug/update']}>

View File

@@ -1,3 +1,5 @@
import '@testing-library/jest-dom/vitest'
import { import {
createMemoryRouter, createMemoryRouter,
createRoutesFromElements, createRoutesFromElements,
@@ -18,9 +20,12 @@ import {
import { FactoryUser } from 'src/test/factories/User' import { FactoryUser } from 'src/test/factories/User'
import { testingThemeStyles } from 'src/test/utils/themeUtils' import { testingThemeStyles } from 'src/test/utils/themeUtils'
import { formatDate } from 'src/utils/date' import { formatDate } from 'src/utils/date'
import { describe, expect, it, vi } from 'vitest'
import ResearchArticle from './ResearchArticle' import ResearchArticle from './ResearchArticle'
import type { Mock } from 'vitest'
const Theme = testingThemeStyles const Theme = testingThemeStyles
const activeUser = FactoryUser({ const activeUser = FactoryUser({
@@ -30,21 +35,21 @@ const activeUser = FactoryUser({
const mockUser = FactoryUser({ country: 'AF' }) const mockUser = FactoryUser({ country: 'AF' })
const mockDiscussionItem = FactoryDiscussion() const mockDiscussionItem = FactoryDiscussion()
jest.mock('src/common/hooks/useCommonStores', () => ({ vi.mock('src/common/hooks/useCommonStores', () => ({
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
__esModule: true, __esModule: true,
useCommonStores: () => ({ useCommonStores: () => ({
stores: { stores: {
userStore: { userStore: {
getUserByUsername: jest.fn().mockResolvedValue(mockUser), getUserByUsername: vi.fn().mockResolvedValue(mockUser),
}, },
aggregationsStore: { aggregationsStore: {
isVerified: jest.fn(), isVerified: vi.fn(),
users_verified: {}, users_verified: {},
}, },
tagsStore: {}, tagsStore: {},
discussionStore: { discussionStore: {
fetchOrCreateDiscussionBySource: jest.fn().mockResolvedValue({ fetchOrCreateDiscussionBySource: vi.fn().mockResolvedValue({
mockDiscussionItem, mockDiscussionItem,
}), }),
activeUser: mockUser, activeUser: mockUser,
@@ -53,16 +58,16 @@ jest.mock('src/common/hooks/useCommonStores', () => ({
}), }),
})) }))
jest.mock('src/stores/Research/research.store') vi.mock('src/stores/Research/research.store')
describe('Research Article', () => { describe('Research Article', () => {
const mockResearchStore = { const mockResearchStore = {
activeResearchItem: FactoryResearchItem(), activeResearchItem: FactoryResearchItem(),
setActiveResearchItemBySlug: jest.fn().mockResolvedValue(true), setActiveResearchItemBySlug: vi.fn().mockResolvedValue(true),
addSubscriberToResearchArticle: jest.fn(), addSubscriberToResearchArticle: vi.fn(),
needsModeration: jest.fn(), needsModeration: vi.fn(),
formatResearchCommentList: jest.fn(), formatResearchCommentList: vi.fn(),
incrementViewCount: jest.fn(), incrementViewCount: vi.fn(),
} }
it('displays content statistics', async () => { it('displays content statistics', async () => {
@@ -77,7 +82,7 @@ describe('Research Article', () => {
], ],
}) })
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeResearchItem, activeResearchItem,
}) })
@@ -100,7 +105,7 @@ describe('Research Article', () => {
it('does not display contributors when undefined', async () => { it('does not display contributors when undefined', async () => {
// Arrange // Arrange
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
collaborators: undefined, collaborators: undefined,
@@ -121,7 +126,7 @@ describe('Research Article', () => {
it('displays contributors', async () => { it('displays contributors', async () => {
// Arrange // Arrange
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
collaborators: ['example-username', 'another-example-username'], collaborators: ['example-username', 'another-example-username'],
@@ -143,7 +148,7 @@ describe('Research Article', () => {
it('displays "Follow" button for non-subscriber', async () => { it('displays "Follow" button for non-subscriber', async () => {
// Arrange // Arrange
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
userHasSubscribed: false, userHasSubscribed: false,
@@ -169,7 +174,7 @@ describe('Research Article', () => {
// TODO: Work out how to simulate store subscribe functionality // TODO: Work out how to simulate store subscribe functionality
// it('displays "Following" button for subscriber', async () => { // it('displays "Following" button for subscriber', async () => {
// // Arrange // // Arrange
// ;(useResearchStore as jest.Mock).mockReturnValue({ // ;(useResearchStore as Mock).mockReturnValue({
// ...mockResearchStore, // ...mockResearchStore,
// activeResearchItem: FactoryResearchItem({ // activeResearchItem: FactoryResearchItem({
// subscribers: [activeUser._id], // subscribers: [activeUser._id],
@@ -192,7 +197,7 @@ describe('Research Article', () => {
describe('Research Update', () => { describe('Research Update', () => {
it('displays contributors', async () => { it('displays contributors', async () => {
// Arrange // Arrange
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
collaborators: ['example-username', 'another-example-username'], collaborators: ['example-username', 'another-example-username'],
@@ -248,9 +253,9 @@ describe('Research Article', () => {
title: 'A title', title: 'A title',
description: 'A description', description: 'A description',
}) })
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
formatResearchCommentList: jest.fn().mockImplementation((c) => { formatResearchCommentList: vi.fn().mockImplementation((c) => {
return c return c
}), }),
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
@@ -279,9 +284,9 @@ describe('Research Article', () => {
description: 'A description', description: 'A description',
_deleted: false, _deleted: false,
}) })
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
formatResearchCommentList: jest.fn().mockImplementation((c) => { formatResearchCommentList: vi.fn().mockImplementation((c) => {
return c return c
}), }),
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
@@ -303,7 +308,7 @@ describe('Research Article', () => {
it('shows only published updates', async () => { it('shows only published updates', async () => {
// Arrange // Arrange
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
collaborators: ['example-username', 'another-example-username'], collaborators: ['example-username', 'another-example-username'],
@@ -336,7 +341,7 @@ describe('Research Article', () => {
describe('Breadcrumbs', () => { describe('Breadcrumbs', () => {
it('displays breadcrumbs with category', async () => { it('displays breadcrumbs with category', async () => {
// Arrange // Arrange
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
title: 'Innovative Study', title: 'Innovative Study',
@@ -377,7 +382,7 @@ describe('Research Article', () => {
it('displays breadcrumbs without category', async () => { it('displays breadcrumbs without category', async () => {
// Arrange // Arrange
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
title: 'Innovative Study', title: 'Innovative Study',

View File

@@ -1,3 +1,5 @@
import { describe, expect, it } from 'vitest'
import { getResearchCommentId } from './helper' import { getResearchCommentId } from './helper'
describe('getReseachCommentId', () => { describe('getReseachCommentId', () => {

View File

@@ -9,7 +9,7 @@ import {
} from 'react-router-dom' } from 'react-router-dom'
import { ThemeProvider } from '@emotion/react' import { ThemeProvider } from '@emotion/react'
import { faker } from '@faker-js/faker' import { faker } from '@faker-js/faker'
import { act, cleanup, render, waitFor } from '@testing-library/react' import { act, cleanup, render } from '@testing-library/react'
import { Provider } from 'mobx-react' import { Provider } from 'mobx-react'
import { UserRole } from 'oa-shared' import { UserRole } from 'oa-shared'
import { useResearchStore } from 'src/stores/Research/research.store' import { useResearchStore } from 'src/stores/Research/research.store'
@@ -19,30 +19,32 @@ import {
} from 'src/test/factories/ResearchItem' } from 'src/test/factories/ResearchItem'
import { FactoryUser } from 'src/test/factories/User' import { FactoryUser } from 'src/test/factories/User'
import { testingThemeStyles } from 'src/test/utils/themeUtils' import { testingThemeStyles } from 'src/test/utils/themeUtils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { researchRouteElements } from './research.routes' import { researchRouteElements } from './research.routes'
import { researchService } from './research.service' import { researchService } from './research.service'
import type { ResearchStore } from 'src/stores/Research/research.store' import type { ResearchStore } from 'src/stores/Research/research.store'
import type { Mock } from 'vitest'
const Theme = testingThemeStyles const Theme = testingThemeStyles
const mockActiveUser = FactoryUser() const mockActiveUser = FactoryUser()
jest.mock('src/pages/Research/research.service') vi.mock('src/pages/Research/research.service')
// Similar to issues in Academy.test.tsx - stub methods called in user store constructor // Similar to issues in Academy.test.tsx - stub methods called in user store constructor
// TODO - replace with mock store or avoid direct call // TODO - replace with mock store or avoid direct call
jest.mock('src/common/hooks/useCommonStores', () => ({ vi.mock('src/common/hooks/useCommonStores', () => ({
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
__esModule: true, __esModule: true,
useCommonStores: () => ({ useCommonStores: () => ({
stores: { stores: {
userStore: { userStore: {
fetchAllVerifiedUsers: jest.fn(), fetchAllVerifiedUsers: vi.fn(),
user: mockActiveUser, user: mockActiveUser,
}, },
aggregationsStore: { aggregationsStore: {
isVerified: jest.fn(), isVerified: vi.fn(),
users_verified: { users_verified: {
HowtoAuthor: true, HowtoAuthor: true,
}, },
@@ -55,17 +57,17 @@ jest.mock('src/common/hooks/useCommonStores', () => ({
}), }),
})) }))
const mockedUsedNavigate = jest.fn() const mockedUsedNavigate = vi.fn()
jest.mock('react-router-dom', () => ({ vi.mock('react-router-dom', async () => ({
...(jest.requireActual('react-router-dom') as any), ...((await vi.importActual('react-router-dom')) as any),
useNavigate: () => mockedUsedNavigate, useNavigate: () => mockedUsedNavigate,
})) }))
/** When mocking research routes replace default store methods with below */ /** When mocking research routes replace default store methods with below */
class MockResearchStoreClass implements Partial<ResearchStore> { class MockResearchStoreClass implements Partial<ResearchStore> {
setActiveResearchItemBySlug = jest.fn() setActiveResearchItemBySlug = vi.fn()
needsModeration = jest.fn().mockResolvedValue(true) needsModeration = vi.fn().mockResolvedValue(true)
incrementViewCount = jest.fn() incrementViewCount = vi.fn()
activeResearchItem = FactoryResearchItem({ activeResearchItem = FactoryResearchItem({
title: 'Research article title', title: 'Research article title',
updates: [], updates: [],
@@ -73,12 +75,12 @@ class MockResearchStoreClass implements Partial<ResearchStore> {
}) })
researchUploadStatus = {} as any researchUploadStatus = {} as any
updateUploadStatus = {} as any updateUploadStatus = {} as any
formatResearchCommentList = jest.fn() formatResearchCommentList = vi.fn()
getActiveResearchUpdateComments = jest.fn() getActiveResearchUpdateComments = vi.fn()
lockResearchItem = jest.fn() lockResearchItem = vi.fn()
lockResearchUpdate = jest.fn() lockResearchUpdate = vi.fn()
unlockResearchUpdate = jest.fn() unlockResearchUpdate = vi.fn()
unlockResearchItem = jest.fn() unlockResearchItem = vi.fn()
get activeUser() { get activeUser() {
return { return {
@@ -93,26 +95,24 @@ class MockResearchStoreClass implements Partial<ResearchStore> {
} }
const mockResearchStore = new MockResearchStoreClass() const mockResearchStore = new MockResearchStoreClass()
jest.mock('src/stores/Research/research.store') vi.mock('src/stores/Research/research.store')
describe('research.routes', () => { describe('research.routes', () => {
beforeEach(() => { beforeEach(() => {
;(useResearchStore as jest.Mock).mockReturnValue(mockResearchStore) ;(useResearchStore as Mock).mockReturnValue(mockResearchStore)
}) })
afterEach(() => { afterEach(() => {
jest.restoreAllMocks() vi.restoreAllMocks()
cleanup() cleanup()
}) })
describe('/research/', () => { describe('/research/', () => {
it('renders the research listing', async () => { it('renders the research listing', async () => {
let wrapper
const researchTitle = faker.lorem.words(3) const researchTitle = faker.lorem.words(3)
const researchSlug = faker.lorem.slug() const researchSlug = faker.lorem.slug()
researchService.search = jest.fn(() => { researchService.search = vi.fn(() => {
return new Promise((resolve) => { return new Promise((resolve) => {
resolve({ resolve({
items: [ items: [
@@ -130,11 +130,9 @@ describe('research.routes', () => {
}) })
}) })
await act(async () => { const wrapper = renderFn('/research')
wrapper = renderFn('/research').wrapper
})
await waitFor( await vi.waitFor(
() => () =>
expect( expect(
wrapper.getByText(/Help out with Research & Development/), wrapper.getByText(/Help out with Research & Development/),
@@ -149,11 +147,11 @@ describe('research.routes', () => {
describe('/research/:slug', () => { describe('/research/:slug', () => {
it('renders an individual research article', async () => { it('renders an individual research article', async () => {
let wrapper let wrapper
await act(async () => { act(() => {
wrapper = renderFn('/research/research-slug').wrapper wrapper = renderFn('/research/research-slug')
}) })
await waitFor( await vi.waitFor(
() => { () => {
expect( expect(
mockResearchStore.setActiveResearchItemBySlug, mockResearchStore.setActiveResearchItemBySlug,
@@ -173,11 +171,11 @@ describe('research.routes', () => {
it('rejects a request without a user present', async () => { it('rejects a request without a user present', async () => {
mockActiveUser.userRoles = [] mockActiveUser.userRoles = []
let wrapper let wrapper
await act(async () => { act(() => {
wrapper = renderFn('/research/create').wrapper wrapper = renderFn('/research/create')
}) })
await waitFor(() => { await vi.waitFor(() => {
expect( expect(
wrapper.getByText(/role required to access this page/), wrapper.getByText(/role required to access this page/),
).toBeInTheDocument() ).toBeInTheDocument()
@@ -186,11 +184,11 @@ describe('research.routes', () => {
it('rejects a logged in user missing required role', async () => { it('rejects a logged in user missing required role', async () => {
let wrapper let wrapper
await act(async () => { act(() => {
wrapper = renderFn('/research/create').wrapper wrapper = renderFn('/research/create')
}) })
await waitFor(() => { await vi.waitFor(() => {
expect( expect(
wrapper.getByText(/role required to access this page/), wrapper.getByText(/role required to access this page/),
).toBeInTheDocument() ).toBeInTheDocument()
@@ -199,13 +197,13 @@ describe('research.routes', () => {
it('accepts a logged in user with required role [research_creator]', async () => { it('accepts a logged in user with required role [research_creator]', async () => {
let wrapper let wrapper
await act(async () => { act(() => {
mockActiveUser.userRoles = [UserRole.RESEARCH_CREATOR] mockActiveUser.userRoles = [UserRole.RESEARCH_CREATOR]
wrapper = renderFn('/research/create').wrapper wrapper = renderFn('/research/create')
}) })
await waitFor( await vi.waitFor(
() => { () => {
expect(wrapper.getByText(/start your research/i)).toBeInTheDocument() expect(wrapper.getByText(/start your research/i)).toBeInTheDocument()
}, },
@@ -217,12 +215,12 @@ describe('research.routes', () => {
it('accepts a logged in user with required role [research_creator]', async () => { it('accepts a logged in user with required role [research_creator]', async () => {
let wrapper let wrapper
await act(async () => { act(() => {
mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR]
wrapper = renderFn('/research/create').wrapper wrapper = renderFn('/research/create')
}) })
await waitFor( await vi.waitFor(
() => { () => {
expect(wrapper.getByText(/start your research/i)).toBeInTheDocument() expect(wrapper.getByText(/start your research/i)).toBeInTheDocument()
}, },
@@ -238,11 +236,11 @@ describe('research.routes', () => {
mockActiveUser.userRoles = [] mockActiveUser.userRoles = []
let wrapper let wrapper
await act(async () => { act(() => {
wrapper = renderFn('/research/an-example/edit').wrapper wrapper = renderFn('/research/an-example/edit')
}) })
await waitFor(() => { await vi.waitFor(() => {
expect( expect(
wrapper.getByText(/role required to access this page/), wrapper.getByText(/role required to access this page/),
).toBeInTheDocument() ).toBeInTheDocument()
@@ -251,14 +249,14 @@ describe('research.routes', () => {
it('accepts a logged in user with required role', async () => { it('accepts a logged in user with required role', async () => {
let wrapper let wrapper
await act(async () => { act(() => {
mockActiveUser.userName = 'Jaasper' mockActiveUser.userName = 'Jaasper'
mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR]
wrapper = renderFn('/research/an-example/edit').wrapper wrapper = renderFn('/research/an-example/edit')
}) })
await waitFor(() => { await vi.waitFor(() => {
expect(wrapper.getByText(/edit your research/i)).toBeInTheDocument() expect(wrapper.getByText(/edit your research/i)).toBeInTheDocument()
}) })
}) })
@@ -267,7 +265,7 @@ describe('research.routes', () => {
mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR]
// Arrange // Arrange
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeUser: mockActiveUser, activeUser: mockActiveUser,
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
@@ -276,18 +274,18 @@ describe('research.routes', () => {
}), }),
}) })
await act(async () => { act(() => {
renderFn('/research/an-example/edit') renderFn('/research/an-example/edit')
}) })
await waitFor(() => { await vi.waitFor(() => {
expect(mockedUsedNavigate).toHaveBeenCalledWith('/research/an-example') expect(mockedUsedNavigate).toHaveBeenCalledWith('/research/an-example')
}) })
}) })
it('blocks a valid editor when document is locked by another user', async () => { it('blocks a valid editor when document is locked by another user', async () => {
mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR]
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeUser: mockActiveUser, activeUser: mockActiveUser,
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
@@ -301,11 +299,11 @@ describe('research.routes', () => {
}) })
let wrapper let wrapper
await act(async () => { act(() => {
wrapper = renderFn('/research/an-example/edit').wrapper wrapper = renderFn('/research/an-example/edit')
}) })
await waitFor(() => { await vi.waitFor(() => {
expect( expect(
wrapper.getByText( wrapper.getByText(
'The research description is currently being edited by another editor.', 'The research description is currently being edited by another editor.',
@@ -316,7 +314,7 @@ describe('research.routes', () => {
it('accepts a user when document is mark locked by them', async () => { it('accepts a user when document is mark locked by them', async () => {
mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR]
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeUser: mockActiveUser, activeUser: mockActiveUser,
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
@@ -330,18 +328,18 @@ describe('research.routes', () => {
}) })
let wrapper let wrapper
await act(async () => { act(() => {
wrapper = renderFn('/research/an-example/edit').wrapper wrapper = renderFn('/research/an-example/edit')
}) })
await waitFor(() => { await vi.waitFor(() => {
expect(wrapper.getByText('Edit your Research')).toBeInTheDocument() expect(wrapper.getByText('Edit your Research')).toBeInTheDocument()
}) })
}) })
it('accepts a user with required role and contributor acccess', async () => { it('accepts a user with required role and contributor acccess', async () => {
mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR]
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeUser: mockActiveUser, activeUser: mockActiveUser,
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
@@ -351,11 +349,11 @@ describe('research.routes', () => {
}) })
let wrapper let wrapper
await act(async () => { act(() => {
wrapper = renderFn('/research/an-example/edit').wrapper wrapper = renderFn('/research/an-example/edit')
}) })
await waitFor(() => { await vi.waitFor(() => {
expect(wrapper.getByText(/edit your research/i)).toBeInTheDocument() expect(wrapper.getByText(/edit your research/i)).toBeInTheDocument()
}) })
}) })
@@ -366,11 +364,11 @@ describe('research.routes', () => {
mockActiveUser.userRoles = [] mockActiveUser.userRoles = []
let wrapper let wrapper
await act(async () => { act(() => {
wrapper = renderFn('/research/an-example/new-update').wrapper wrapper = renderFn('/research/an-example/new-update')
}) })
await waitFor(() => { await vi.waitFor(() => {
expect( expect(
wrapper.getByText(/role required to access this page/), wrapper.getByText(/role required to access this page/),
).toBeInTheDocument() ).toBeInTheDocument()
@@ -379,12 +377,12 @@ describe('research.routes', () => {
it('accepts a logged in user with required role', async () => { it('accepts a logged in user with required role', async () => {
let wrapper let wrapper
await act(async () => { act(() => {
mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR]
wrapper = renderFn('/research/an-example/new-update')
wrapper = renderFn('/research/an-example/new-update').wrapper
}) })
await waitFor(() => {
await vi.waitFor(() => {
expect(wrapper.getByTestId('EditResearchUpdate')).toBeInTheDocument() expect(wrapper.getByTestId('EditResearchUpdate')).toBeInTheDocument()
}) })
}) })
@@ -393,14 +391,11 @@ describe('research.routes', () => {
describe('/research/:slug/edit-update/:id', () => { describe('/research/:slug/edit-update/:id', () => {
it('rejects a request without a user present', async () => { it('rejects a request without a user present', async () => {
mockActiveUser.userRoles = [] mockActiveUser.userRoles = []
let wrapper const wrapper = renderFn(
await act(async () => { '/research/an-example/edit-update/nested-research-update',
wrapper = renderFn( )
'/research/an-example/edit-update/nested-research-update',
).wrapper
})
await waitFor(() => { await vi.waitFor(() => {
expect( expect(
wrapper.getByText(/role required to access this page/), wrapper.getByText(/role required to access this page/),
).toBeInTheDocument() ).toBeInTheDocument()
@@ -410,7 +405,7 @@ describe('research.routes', () => {
it('accept logged in author present', async () => { it('accept logged in author present', async () => {
mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR]
// Arrange // Arrange
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeUser: mockActiveUser, activeUser: mockActiveUser,
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
@@ -426,13 +421,13 @@ describe('research.routes', () => {
}) })
let wrapper let wrapper
await act(async () => { act(() => {
wrapper = renderFn( wrapper = renderFn(
'/research/an-example/edit-update/nested-research-update', '/research/an-example/edit-update/nested-research-update',
).wrapper )
}) })
await waitFor(() => { await vi.waitFor(() => {
expect(wrapper.getByTestId(/EditResearchUpdate/i)).toBeInTheDocument() expect(wrapper.getByTestId(/EditResearchUpdate/i)).toBeInTheDocument()
}) })
}) })
@@ -440,7 +435,7 @@ describe('research.routes', () => {
it('blocks valid author when document is locked', async () => { it('blocks valid author when document is locked', async () => {
// Arrange // Arrange
mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR]
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeUser: mockActiveUser, activeUser: mockActiveUser,
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
@@ -459,11 +454,11 @@ describe('research.routes', () => {
}), }),
}) })
const { wrapper } = renderFn( const wrapper = renderFn(
'/research/an-example/edit-update/nested-research-update', '/research/an-example/edit-update/nested-research-update',
) )
await waitFor(() => { await vi.waitFor(() => {
expect( expect(
wrapper.getByText( wrapper.getByText(
/This research update is currently being edited by another editor/, /This research update is currently being edited by another editor/,
@@ -474,7 +469,7 @@ describe('research.routes', () => {
it('accepts a user when document is mark locked by them', async () => { it('accepts a user when document is mark locked by them', async () => {
mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR]
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeUser: mockActiveUser, activeUser: mockActiveUser,
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
@@ -492,11 +487,11 @@ describe('research.routes', () => {
}), }),
}) })
const { wrapper } = renderFn( const wrapper = renderFn(
'/research/an-example/edit-update/nested-research-update', '/research/an-example/edit-update/nested-research-update',
) )
await waitFor(() => { await vi.waitFor(() => {
expect(wrapper.getByText('Edit your update')).toBeInTheDocument() expect(wrapper.getByText('Edit your update')).toBeInTheDocument()
}) })
}) })
@@ -505,13 +500,13 @@ describe('research.routes', () => {
mockActiveUser.userRoles = [] mockActiveUser.userRoles = []
let wrapper let wrapper
await act(async () => { act(() => {
wrapper = renderFn( wrapper = renderFn(
'/research/an-example/edit-update/nested-research-update', '/research/an-example/edit-update/nested-research-update',
).wrapper )
}) })
await waitFor(() => { await vi.waitFor(() => {
expect( expect(
wrapper.getByText(/role required to access this page/), wrapper.getByText(/role required to access this page/),
).toBeInTheDocument() ).toBeInTheDocument()
@@ -522,7 +517,7 @@ describe('research.routes', () => {
mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR]
// Arrange // Arrange
;(useResearchStore as jest.Mock).mockReturnValue({ ;(useResearchStore as Mock).mockReturnValue({
...mockResearchStore, ...mockResearchStore,
activeUser: mockActiveUser, activeUser: mockActiveUser,
activeResearchItem: FactoryResearchItem({ activeResearchItem: FactoryResearchItem({
@@ -537,13 +532,13 @@ describe('research.routes', () => {
}) })
let wrapper let wrapper
await act(async () => { act(() => {
wrapper = renderFn( wrapper = renderFn(
'/research/an-example/edit-update/nested-research-update', '/research/an-example/edit-update/nested-research-update',
).wrapper )
}) })
await waitFor(() => { await vi.waitFor(() => {
expect(wrapper.getByTestId(/EditResearchUpdate/i)).toBeInTheDocument() expect(wrapper.getByTestId(/EditResearchUpdate/i)).toBeInTheDocument()
}) })
}) })
@@ -560,15 +555,13 @@ const renderFn = (url: string) => {
}, },
) )
return { return render(
wrapper: render( <Suspense fallback={<></>}>
<Suspense fallback={<></>}> <Provider userStore={{ user: mockActiveUser }} tagsStore={{}}>
<Provider userStore={{ user: mockActiveUser }} tagsStore={{}}> <ThemeProvider theme={Theme}>
<ThemeProvider theme={Theme}> <RouterProvider router={router} />
<RouterProvider router={router} /> </ThemeProvider>
</ThemeProvider> </Provider>
</Provider> </Suspense>,
</Suspense>, )
),
}
} }

View File

@@ -1,37 +1,39 @@
import '@testing-library/jest-dom' import '@testing-library/jest-dom'
import { ResearchStatus } from 'oa-shared' import { ResearchStatus } from 'oa-shared'
import { describe, expect, it, vi } from 'vitest'
import { exportedForTesting } from './research.service' import { exportedForTesting } from './research.service'
const mockWhere = jest.fn() const mockWhere = vi.fn()
const mockOrderBy = jest.fn() const mockOrderBy = vi.fn()
const mockLimit = jest.fn() const mockLimit = vi.fn()
jest.mock('firebase/firestore', () => ({ vi.mock('firebase/firestore', () => ({
collection: jest.fn(), collection: vi.fn(),
query: jest.fn(), query: vi.fn(),
and: jest.fn(), and: vi.fn(),
where: (path, op, value) => mockWhere(path, op, value), where: (path, op, value) => mockWhere(path, op, value),
limit: (limit) => mockLimit(limit), limit: (limit) => mockLimit(limit),
orderBy: (field, direction) => mockOrderBy(field, direction), orderBy: (field, direction) => mockOrderBy(field, direction),
})) }))
jest.mock('../../stores/databaseV2/endpoints', () => ({ vi.mock('../../stores/databaseV2/endpoints', () => ({
DB_ENDPOINTS: { DB_ENDPOINTS: {
research: 'research', research: 'research',
researchCategories: 'researchCategories', researchCategories: 'researchCategories',
}, },
})) }))
jest.mock('../../config/config', () => ({ vi.mock('../../config/config', () => ({
getConfigurationOption: jest.fn(), getConfigurationOption: vi.fn(),
FIREBASE_CONFIG: { FIREBASE_CONFIG: {
apiKey: 'AIyChVN', apiKey: 'AIyChVN',
databaseURL: 'https://test.firebaseio.com', databaseURL: 'https://test.firebaseio.com',
projectId: 'test', projectId: 'test',
storageBucket: 'test.appspot.com', storageBucket: 'test.appspot.com',
}, },
localStorage: jest.fn(), localStorage: vi.fn(),
SITE: 'unit-tests',
})) }))
describe('research.search', () => { describe('research.search', () => {

View File

@@ -1,4 +1,4 @@
jest.mock('../../stores/common/module.store') import '@testing-library/jest-dom/vitest'
import { import {
createMemoryRouter, createMemoryRouter,
@@ -10,16 +10,19 @@ import { ThemeProvider } from '@emotion/react'
import { render, waitFor } from '@testing-library/react' import { render, waitFor } from '@testing-library/react'
import { Provider } from 'mobx-react' import { Provider } from 'mobx-react'
import { testingThemeStyles } from 'src/test/utils/themeUtils' import { testingThemeStyles } from 'src/test/utils/themeUtils'
import { describe, expect, it, vi } from 'vitest'
import Unsubscribe from './Unsubscribe' import Unsubscribe from './Unsubscribe'
vi.mock('../../stores/common/module.store')
const Theme = testingThemeStyles const Theme = testingThemeStyles
const mockUnsubscribeUser = jest.fn() const mockUnsubscribeUser = vi.fn()
// Similar to issues in Academy.test.tsx - stub methods called in user store constructor // Similar to issues in Academy.test.tsx - stub methods called in user store constructor
// TODO - replace with mock store or avoid direct call // TODO - replace with mock store or avoid direct call
jest.mock('src/common/hooks/useCommonStores', () => ({ vi.mock('src/common/hooks/useCommonStores', () => ({
__esModule: true, __esModule: true,
useCommonStores: () => ({ useCommonStores: () => ({
stores: { stores: {
@@ -28,7 +31,7 @@ jest.mock('src/common/hooks/useCommonStores', () => ({
unsubscribeUser: mockUnsubscribeUser, unsubscribeUser: mockUnsubscribeUser,
}, },
aggregationsStore: { aggregationsStore: {
isVerified: jest.fn(), isVerified: vi.fn(),
users_verified: { users_verified: {
HowtoAuthor: true, HowtoAuthor: true,
}, },

Some files were not shown because too many files have changed in this diff Show More