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:
name: Set branch environment
command: |
echo 'export REACT_APP_PROJECT_VERSION=${CIRCLE_SHA1}' >> $BASH_ENV
echo 'export VITE_APP_PROJECT_VERSION=${CIRCLE_SHA1}' >> $BASH_ENV
- run:
name: Inject target environment configuration
command: |
@@ -200,7 +200,6 @@ jobs:
resource_class: large
environment:
CYPRESS_INSTALL_BINARY: 0
JEST_JUNIT_OUTPUT_DIR: ./reports
steps:
- setup_repo
- attach_workspace:
@@ -249,8 +248,6 @@ jobs:
- run:
# 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
environment:
JEST_JUNIT_OUTPUT_DIR: reports
- store_artifacts:
path: coverage
- store_artifacts:
@@ -288,13 +285,13 @@ jobs:
- run:
name: Set branch environment
command: |
echo 'export REACT_APP_BRANCH=${CIRCLE_BRANCH}' >> $BASH_ENV
echo 'export REACT_APP_PROJECT_VERSION=${CIRCLE_SHA1}' >> $BASH_ENV
echo 'export VITE_APP_BRANCH=${CIRCLE_BRANCH}' >> $BASH_ENV
echo 'export VITE_APP_PROJECT_VERSION=${CIRCLE_SHA1}' >> $BASH_ENV
- run:
name: Check environment variables
command: |
echo REACT_APP_BRANCH=$REACT_APP_BRANCH
echo $REACT_APP_PROJECT_VERSION
echo VITE_APP_BRANCH=$VITE_APP_BRANCH
echo $VITE_APP_PROJECT_VERSION
- run:
command: yarn build
- 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",
"simple-import-sort",
"sort-class-members",
"jest"
"vitest"
],
"extends": [
"eslint:recommended",
@@ -104,7 +104,6 @@
"prefer": "type-imports"
}
],
"jest/no-focused-tests": "error",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/interface-name-prefix": "off",
@@ -133,7 +132,7 @@
"camelCase": 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",

View File

@@ -79,7 +79,7 @@ jobs:
- name: Install npm dependencies
run: yarn install --immutable
- name: Set environment variables
run: export REACT_APP_PROJECT_VERSION=${GITHUB_SHA}
run: export VITE_APP_PROJECT_VERSION=${GITHUB_SHA}
- name: Prepare Build
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

View File

@@ -45,9 +45,9 @@ jobs:
- name: Install npm dependencies
run: yarn install --immutable
- name: Set environment variables
run: export REACT_APP_PROJECT_VERSION=${GITHUB_SHA}
run: export VITE_APP_PROJECT_VERSION=${GITHUB_SHA}
- name: Check environment variables
run: echo $REACT_APP_PROJECT_VERSION
run: echo $VITE_APP_PROJECT_VERSION
- name: Build for Preview
run: npm run build
env:
@@ -55,7 +55,7 @@ jobs:
# disable until fully resolved
CI: false
# 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
# gives us control over what version will be made available
- name: Install firebase-tools globally

View File

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

View File

@@ -1,5 +1,4 @@
/* global module */
module.exports = {
export default {
extends: ['@commitlint/config-conventional'],
/** 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

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,
"main": "lib/index.js",
"type": "module",
"scripts": {
"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: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: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=\"",
"build:themes": "yarn workspace oa-themes 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:inject-config": "yarn build:post",
"build:shared": "yarn workspace oa-shared build",
"build": "yarn build:themes && yarn build:components && yarn build:shared && yarn build:cra",
"eject": "react-scripts eject",
"build": "yarn build:themes && yarn build:components && yarn build:shared && yarn build:vite",
"lint": "yarn lint:style && yarn lint:code",
"lint:commits": " npx commitlint --from=$(git merge-base master HEAD) --verbose",
"lint:code": "eslint . --ext .js,.jsx,.ts,.tsx src --color",
@@ -39,7 +39,7 @@
"serve": "npx serve -s build",
"test": "yarn workspace oa-cypress start",
"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",
"storybook": "yarn workspace oa-components start",
"storybook:build": "yarn build:themes && yarn workspace oa-components build:sb",
@@ -75,9 +75,13 @@
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.8.1",
"@sentry/react": "^6.15.0",
"@uppy/core": "^2.1.4",
"@uppy/dashboard": "^2.1.3",
"@uppy/react": "^2.1.2",
"@uppy/compressor": "^1.1.4",
"@uppy/core": "^3.11.3",
"@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",
"date-fns": "^3.3.0",
"debounce": "^1.2.0",
@@ -99,6 +103,7 @@
"oa-shared": "workspace:*",
"oa-themes": "workspace:*",
"react": "17.0.2",
"react-country-flag": "^3.1.0",
"react-dom": "17.0.2",
"react-dropzone": "^10.1.10",
"react-final-form": "6.5.3",
@@ -110,8 +115,6 @@
"react-leaflet-markercluster": "^2.0.0-rc3",
"react-router": "^6.20.1",
"react-router-dom": "^6.20.1",
"react-scripts": "5.0.1",
"react-virtualized": "9.22.5",
"rxjs": "^6.6.3",
"theme-ui": "^0.15.7",
"ts-node": "^10.4.0",
@@ -119,20 +122,19 @@
"yup": "^1.3.2"
},
"devDependencies": {
"@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^16.2.1",
"@commitlint/cz-commitlint": "^16.2.3",
"@craco/craco": "^7.0.0",
"@craco/types": "^7.0.0",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@commitlint/cz-commitlint": "^19.2.0",
"@emotion/babel-plugin": "^11.11.0",
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@faker-js/faker": "^8.4.1",
"@semantic-release/changelog": "^6.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/user-event": "^14.4.3",
"@types/browser-image-compression": "^1.0.9",
"@types/flux-standard-action": "1.1.0",
"@types/jest": "^27.4.1",
"@types/node": "^20.8.0",
"@types/pubsub-js": "^1.5.18",
"@types/react": "17.0.2",
@@ -141,10 +143,11 @@
"@types/react-leaflet-markercluster": "^2.0.0",
"@types/react-router-dom": "5.3.3",
"@types/react-select": "^2.0.17",
"@types/react-virtualized": "^9.21.10",
"@types/styled-system": "^5.1.11",
"@typescript-eslint/eslint-plugin": "^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",
"buffer": "^6.0.3",
"chai-subset": "^1.6.0",
@@ -158,16 +161,16 @@
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^27.4.2",
"eslint-plugin-mocha": "^10.2.0",
"eslint-plugin-prefer-arrow-functions": "^3.1.4",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "^7.34.2",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-sort-class-members": "^1.18.0",
"eslint-plugin-unicorn": "^48.0.1",
"husky": "^7.0.4",
"eslint-plugin-vitest": "^0.5.4",
"husky": "^9.0.11",
"idb": "^6.0.0",
"jest-junit": "^16.0.0",
"lint-staged": "^8.1.5",
"mobx-react-devtools": "^6.0.3",
"prettier": "2.5.1",
@@ -176,9 +179,13 @@
"start-server-and-test": "^1.11.0",
"stream-browserify": "^3.0.0",
"terser": "3.14.1",
"ts-jest": "^29.1.2",
"ts-loader": "^7.0.5",
"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",
"workbox-background-sync": "^6.1.5",
"workbox-broadcast-update": "^6.1.5",

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,7 @@
import '@testing-library/jest-dom'
import { fireEvent, waitFor } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils'
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 { fireEvent } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils'
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 { COMMENTS, NO_COMMENTS, ONE_COMMENT } from './DiscussionTitle'
import {

View File

@@ -1,5 +1,5 @@
import { fireEvent } from '@testing-library/react'
import { vi } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { render } from '../tests/utils'
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 { 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 { 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 { 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 { Default } from './DownloadFileFromLink.stories'

View File

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

View File

@@ -1,30 +1,16 @@
import * as React from 'react'
import { ReactCountryFlag } from 'react-country-flag'
import styled from '@emotion/styled'
import { Box } from 'theme-ui'
import FlagIconFactory from 'react-flag-icon-css'
// 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)`
export const FlagIconEvents = styled(ReactCountryFlag)`
border-radius: 5px;
background-size: cover !important;
height: 23px;
width: 35px !important;
`
/*
@media only screen and (max-width: ${(props) => props.theme.breakpoints[1]}) {
height: 15px;
width: 25px !important;
}
*/
export const FlagIconHowTos = styled(FlagIconFact)`
export const FlagIconHowTos = styled(ReactCountryFlag)`
border-radius: 3px;
background-size: cover !important;
height: 14px;
@@ -33,6 +19,8 @@ export const FlagIconHowTos = styled(FlagIconFact)`
export const FlagIcon = (props: 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>
)

View File

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

View File

@@ -1,5 +1,7 @@
import '@testing-library/jest-dom'
import { fireEvent } from '@testing-library/react'
import { vi } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { render } from '../tests/utils'
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
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 { 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 { vi } from 'vitest'
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { render } from '../tests/utils'
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 { MapMemberCard } from './MapMemberCard'
import { Default, ModerationComments } from './MapMemberCard.stories'

View File

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

View File

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

View File

@@ -5,8 +5,8 @@ import { Box, Card, Text } from 'theme-ui'
import { Button } from '../Button/Button'
import { NotificationItem } from '../NotificationItem/NotificationItem'
import type { UserNotificationItem } from 'oa-shared'
import type { ThemeUIStyleObject } from 'theme-ui'
import type { UserNotificationItem } from '../NotificationItem/NotificationItem'
export interface Props {
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 { 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 {
Default,

View File

@@ -1,4 +1,7 @@
import '@testing-library/jest-dom'
import { act } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { render } from '../tests/utils'
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 { 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 { 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 { 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 { Username } from './Username'
import {

View File

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

View File

@@ -57,4 +57,3 @@ export { UserEngagementWrapper } from './UserEngagementWrapper/UserEngagementWra
export { IconCountWithTooltip } from './IconCountWithTooltip/IconCountWithTooltip'
export { DonationRequest } from './DonationRequest/DonationRequest'
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() {
const { CROSSENV_BIN, BUILD_SERVE_JSON } = PATHS
// 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
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_
* 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

View File

@@ -44,29 +44,29 @@ See [circleci slack orb](https://github.com/CircleCI-Public/slack-orb) for info)
### 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
```
REACT_APP_FIREBASE_API_KEY
REACT_APP_FIREBASE_AUTH_DOMAIN
REACT_APP_FIREBASE_DATABASE_URL
REACT_APP_FIREBASE_MESSAGING_SENDER_ID
REACT_APP_FIREBASE_PROJECT_ID
REACT_APP_FIREBASE_STORAGE_BUCKET
VITE_APP_FIREBASE_API_KEY
VITE_APP_FIREBASE_AUTH_DOMAIN
VITE_APP_FIREBASE_DATABASE_URL
VITE_APP_FIREBASE_MESSAGING_SENDER_ID
VITE_APP_FIREBASE_PROJECT_ID
VITE_APP_FIREBASE_STORAGE_BUCKET
```
Sentry error tracking
```
REACT_APP_SENTRY_DSN
VITE_APP_SENTRY_DSN
```
Google Analytics
```
REACT_APP_GA_TRACKING_ID
VITE_APP_GA_TRACKING_ID
```
### 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`
- `GOOGLE_APPLICATION_CREDENTIALS_JSON`
- `REACT_APP_BRANCH`
- `REACT_APP_FIREBASE_API_KEY`
- `REACT_APP_FIREBASE_AUTH_DOMAIN`
- `REACT_APP_FIREBASE_DATABASE_URL`
- `REACT_APP_FIREBASE_MESSAGING_SENDER_ID`
- `REACT_APP_FIREBASE_PROJECT_ID`
- `REACT_APP_FIREBASE_STORAGE_BUCKET`
- `REACT_APP_GA_TRACKING_ID`
- `REACT_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.
- `REACT_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.
- `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_BRANCH`
- `VITE_APP_FIREBASE_API_KEY`
- `VITE_APP_FIREBASE_AUTH_DOMAIN`
- `VITE_APP_FIREBASE_DATABASE_URL`
- `VITE_APP_FIREBASE_MESSAGING_SENDER_ID`
- `VITE_APP_FIREBASE_PROJECT_ID`
- `VITE_APP_FIREBASE_STORAGE_BUCKET`
- `VITE_APP_GA_TRACKING_ID`
- `VITE_APP_PLATFORM_THEME`
- `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.
- `VITE_APP_PLATFORM_PROFILES` - comma separated list of available profiles. Use `ProfileType` from modules/profile/index for guidance here. For example: `member,workspace`
- `VITE_APP_SUPPORTED_MODULES`  comma separated list of available modules. See `/src/modules/index.ts` for the definitions.
- `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`

View File

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

View File

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

View File

@@ -55,7 +55,7 @@ const storage =
* e.g. oa_
* 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

View File

@@ -23,3 +23,8 @@ export const NotificationTypes = [
] as const
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 { UserRole } from 'oa-shared'
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
@@ -10,7 +11,7 @@ const mockUser = FactoryUser({
userRoles: [UserRole.ADMIN],
})
jest.mock('src/common/hooks/useCommonStores', () => ({
vi.mock('src/common/hooks/useCommonStores', () => ({
__esModule: true,
useCommonStores: () => ({
stores: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
import '@testing-library/jest-dom/vitest'
import { render } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { TagList } from './TagsList'
jest.mock('src/common/hooks/useCommonStores', () => ({
vi.mock('src/common/hooks/useCommonStores', () => ({
__esModule: true,
useCommonStores: () => ({
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
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
'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:
https://javebratt.com/hide-firebase-api/
@@ -17,7 +17,7 @@ import type { IFirebaseConfig, ISentryConfig, siteVariants } from './types'
/**
* Helper function to load configuration property
* 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`
* 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 configurationSource = ['development', 'test'].includes(
process.env.NODE_ENV,
import.meta.env.NODE_ENV,
)
? process.env
? import.meta.env
: window?.__OA_COMMUNITY_PLATFORM_CONFIGURATION
return configurationSource?.[property] || fallbackValue
}
@@ -62,14 +62,14 @@ const getSiteVariant = (): siteVariants => {
}
if (
location.host === 'localhost:3456' ||
_c('REACT_APP_SITE_VARIANT') === 'test-ci'
_c('VITE_APP_SITE_VARIANT') === 'test-ci'
) {
return 'test-ci'
}
if (_c('REACT_APP_SITE_VARIANT') === 'preview') {
if (_c('VITE_APP_SITE_VARIANT') === 'preview') {
return 'preview'
}
switch (_c('REACT_APP_BRANCH')) {
switch (_c('VITE_APP_BRANCH')) {
case 'production':
return 'production'
case 'master':
@@ -131,12 +131,12 @@ const firebaseConfigs: { [variant in siteVariants]: IFirebaseConfig } = {
},
/** Production/live backend with released frontend */
production: {
apiKey: _c('REACT_APP_FIREBASE_API_KEY'),
authDomain: _c('REACT_APP_FIREBASE_AUTH_DOMAIN'),
databaseURL: _c('REACT_APP_FIREBASE_DATABASE_URL'),
messagingSenderId: _c('REACT_APP_FIREBASE_MESSAGING_SENDER_ID'),
projectId: _c('REACT_APP_FIREBASE_PROJECT_ID'),
storageBucket: _c('REACT_APP_FIREBASE_STORAGE_BUCKET'),
apiKey: _c('VITE_APP_FIREBASE_API_KEY'),
authDomain: _c('VITE_APP_FIREBASE_AUTH_DOMAIN'),
databaseURL: _c('VITE_APP_FIREBASE_DATABASE_URL'),
messagingSenderId: _c('VITE_APP_FIREBASE_MESSAGING_SENDER_ID'),
projectId: _c('VITE_APP_FIREBASE_PROJECT_ID'),
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 SENTRY_CONFIG: ISentryConfig = {
dsn: _c(
'REACT_APP_SENTRY_DSN',
'VITE_APP_SENTRY_DSN',
'https://8c1f7eb4892e48b18956af087bdfa3ac@sentry.io/1399729',
),
environment: siteVariant,
}
export const CDN_URL = _c('REACT_APP_CDN_URL', '')
export const VERSION = _c('REACT_APP_PROJECT_VERSION', '')
export const GA_TRACKING_ID = _c('REACT_APP_GA_TRACKING_ID')
export const PATREON_CLIENT_ID = _c('REACT_APP_PATREON_CLIENT_ID')
export const API_URL = _c('REACT_APP_API_URL', '')
export const CDN_URL = _c('VITE_APP_CDN_URL', '')
export const VERSION = _c('VITE_APP_PROJECT_VERSION', '')
export const GA_TRACKING_ID = _c('VITE_APP_GA_TRACKING_ID')
export const PATREON_CLIENT_ID = _c('VITE_APP_PATREON_CLIENT_ID')
export const API_URL = _c('VITE_APP_API_URL', '')
export const isPreciousPlastic = (): boolean => {
return (
(_c('REACT_APP_PLATFORM_THEME') ||
localStorage.getItem('platformTheme')) === 'precious-plastic'
(_c('VITE_APP_PLATFORM_THEME') || localStorage.getItem('platformTheme')) ===
'precious-plastic'
)
}

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,15 @@
import { afterAll, describe, expect, it } from 'vitest'
import { getSupportedModules, isModuleSupported, MODULE } from '.'
describe('getSupportedModules', () => {
const oldProcessEnv = process.env
const defaultModules = import.meta.env.VITE_APP_SUPPORTED_MODULES
afterAll(() => {
process.env = oldProcessEnv
import.meta.env.VITE_APP_SUPPORTED_MODULES = defaultModules
})
it('returns a default set of modules', () => {
process.env.REACT_APP_SUPPORTED_MODULES = ''
import.meta.env.VITE_APP_SUPPORTED_MODULES = ''
expect(getSupportedModules()).toStrictEqual([
MODULE.CORE,
MODULE.HOWTO,
@@ -20,17 +22,17 @@ describe('getSupportedModules', () => {
})
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])
})
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])
})
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])
})
})
@@ -41,12 +43,12 @@ describe('isModuleSupported', () => {
})
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)
})
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)
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react'
import { ResearchProvider } from 'src/test/components'
import { describe, expect, it } from 'vitest'
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 { 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 { testingThemeStyles } from 'src/test/utils/themeUtils'
import { describe, expect, it, vi } from 'vitest'
import { ResearchUpdateForm } from './ResearchUpdate.form'
const Theme = testingThemeStyles
jest.mock('src/stores/Research/research.store', () => {
vi.mock('src/stores/Research/research.store', () => {
return {
useResearchStore: () => ({
updateUploadStatus: {
@@ -19,8 +21,8 @@ jest.mock('src/stores/Research/research.store', () => {
Database: false,
Complete: false,
},
isTitleThatReusesSlug: jest.fn(),
unlockResearchUpdate: jest.fn(),
isTitleThatReusesSlug: vi.fn(),
unlockResearchUpdate: vi.fn(),
}),
}
})
@@ -32,10 +34,7 @@ describe('Research update form', () => {
fileLink: 'www.filedonwload.test',
})
let wrapper
await act(async () => {
wrapper = await getWrapper(formValues, 'create', {})
})
const wrapper = getWrapper(formValues, 'create', {})
expect(
wrapper.queryByTestId('invalid-file-warning'),
@@ -49,41 +48,17 @@ describe('Research update form', () => {
})
// Act
let wrapper
await act(async () => {
wrapper = await getWrapper(formValues, 'create', {})
})
const wrapper = getWrapper(formValues, 'create', {})
// Assert
expect(
wrapper.queryByTestId('invalid-file-warning'),
).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(
<ThemeProvider theme={Theme}>
<MemoryRouter initialEntries={['/research/:slug/update']}>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
jest.mock('../../stores/common/module.store')
import '@testing-library/jest-dom/vitest'
import {
createMemoryRouter,
@@ -10,16 +10,19 @@ import { ThemeProvider } from '@emotion/react'
import { render, waitFor } from '@testing-library/react'
import { Provider } from 'mobx-react'
import { testingThemeStyles } from 'src/test/utils/themeUtils'
import { describe, expect, it, vi } from 'vitest'
import Unsubscribe from './Unsubscribe'
vi.mock('../../stores/common/module.store')
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
// TODO - replace with mock store or avoid direct call
jest.mock('src/common/hooks/useCommonStores', () => ({
vi.mock('src/common/hooks/useCommonStores', () => ({
__esModule: true,
useCommonStores: () => ({
stores: {
@@ -28,7 +31,7 @@ jest.mock('src/common/hooks/useCommonStores', () => ({
unsubscribeUser: mockUnsubscribeUser,
},
aggregationsStore: {
isVerified: jest.fn(),
isVerified: vi.fn(),
users_verified: {
HowtoAuthor: true,
},

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