Adds vitest for testing client code (#1055)

This commit is contained in:
Shayne Czyzewski 2023-03-30 12:22:40 -04:00 committed by GitHub
parent 6dbeedca60
commit 5d2b278e75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
106 changed files with 1277 additions and 176 deletions

View File

@ -58,6 +58,9 @@ That it, all you need to do is run `wasp start db` and you are good to go. No en
NOTE: Requires `docker` to be installed.
### `wasp test client` -> Wasp can now test your web app code
By leveraging Vitest and some supporting libraries, Wasp now makes it super easy to add unit tests and React component tests to your frontend codebase.
### `pg-boss` upgraded to latest version (8.4.2)
This `pg-boss` release fixes an issue where the node server would exit due to an unhandled exception when the DB connection was lost.

View File

@ -26,6 +26,7 @@ import Wasp.Cli.Command.Info (info)
import Wasp.Cli.Command.Start (start)
import qualified Wasp.Cli.Command.Start.Db as Command.Start.Db
import qualified Wasp.Cli.Command.Telemetry as Telemetry
import Wasp.Cli.Command.Test (test)
import Wasp.Cli.Command.Uninstall (uninstall)
import Wasp.Cli.Command.WaspLS (runWaspLS)
import Wasp.Cli.Terminal (title)
@ -55,6 +56,7 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do
["completion:list"] -> Command.Call.BashCompletionListCommands
("waspls" : _) -> Command.Call.WaspLS
("deploy" : deployArgs) -> Command.Call.Deploy deployArgs
("test" : testArgs) -> Command.Call.Test testArgs
_ -> Command.Call.Unknown args
telemetryThread <- Async.async $ runCommand $ Telemetry.considerSendingData commandCall
@ -79,6 +81,7 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do
Command.Call.Unknown _ -> printUsage
Command.Call.WaspLS -> runWaspLS
Command.Call.Deploy deployArgs -> runCommand $ deploy deployArgs
Command.Call.Test testArgs -> runCommand $ test testArgs
-- If sending of telemetry data is still not done 1 second since commmand finished, abort it.
-- We also make sure here to catch all errors that might get thrown and silence them.
@ -110,7 +113,7 @@ printUsage =
cmd " start Runs Wasp app in development mode, watching for file changes.",
cmd " start db Starts managed development database for you.",
cmd " db <db-cmd> [args] Executes a database command. Run 'wasp db' for more info.",
cmd $ " clean Deletes all generated code and other cached artifacts.",
cmd " clean Deletes all generated code and other cached artifacts.",
" Wasp equivalent of 'have you tried closing and opening it again?'.",
cmd " build Generates full web app code, ready for deployment. Use when deploying or ejecting.",
cmd " deploy Deploys your Wasp app to cloud hosting providers.",
@ -118,6 +121,7 @@ printUsage =
cmd " deps Prints the dependencies that Wasp uses in your project.",
cmd " dockerfile Prints the contents of the Wasp generated Dockerfile.",
cmd " info Prints basic information about current Wasp project.",
cmd " test Executes tests in your project.",
"",
title "EXAMPLES",
" wasp new MyApp",

View File

@ -19,4 +19,5 @@ data Call
| BashCompletionListCommands
| WaspLS
| Deploy [String] -- deploy cmd passthrough args
| Test [String] -- "client" | "server", then test cmd passthrough args
| Unknown [String] -- all args

View File

@ -0,0 +1,48 @@
module Wasp.Cli.Command.Test
( test,
)
where
import Control.Concurrent.Async (race)
import Control.Concurrent.MVar (newMVar)
import Control.Monad.Except (throwError)
import Control.Monad.IO.Class (liftIO)
import StrongPath (Abs, Dir, (</>))
import StrongPath.Types (Path')
import Wasp.Cli.Command (Command, CommandError (..))
import Wasp.Cli.Command.Common (findWaspProjectRootDirFromCwd)
import Wasp.Cli.Command.Compile (compile)
import Wasp.Cli.Command.Message (cliSendMessageC)
import Wasp.Cli.Command.Watch (watch)
import qualified Wasp.Cli.Common as Common
import qualified Wasp.Generator
import Wasp.Generator.Common (ProjectRootDir)
import qualified Wasp.Message as Msg
test :: [String] -> Command ()
test [] = throwError $ CommandError "Not enough arguments" "Expected: wasp test client <args>"
test ("client" : args) = watchAndTest $ Wasp.Generator.testWebApp args
test ("server" : _args) = throwError $ CommandError "Invalid arguments" "Server testing not yet implemented."
test _ = throwError $ CommandError "Invalid arguments" "Expected: wasp test client <args>"
watchAndTest :: (Path' Abs (Dir ProjectRootDir) -> IO (Either String ())) -> Command ()
watchAndTest testRunner = do
waspRoot <- findWaspProjectRootDirFromCwd
let outDir = waspRoot </> Common.dotWaspDirInWaspProjectDir </> Common.generatedCodeDirInDotWaspDir
cliSendMessageC $ Msg.Start "Starting compilation and setup phase. Hold tight..."
warnings <- compile
cliSendMessageC $ Msg.Start "Watching for file changes and running tests ..."
watchOrStartResult <- liftIO $ do
ongoingCompilationResultMVar <- newMVar (warnings, [])
let watchWaspProjectSource = watch waspRoot outDir ongoingCompilationResultMVar
watchWaspProjectSource `race` testRunner outDir
case watchOrStartResult of
Left () -> error "This should never happen, listening for file changes should never end but it did."
Right startResult -> case startResult of
Left testError -> throwError $ CommandError "Test failed" testError
Right () -> return ()

View File

@ -1,10 +1,12 @@
import { callOperation } from '../operations'
import { callOperation, makeOperationRoute } from '../operations'
import {
registerActionInProgress,
registerActionDone,
} from '../operations/resources'
export function createAction(actionRoute, entitiesUsed) {
export function createAction(relativeActionRoute, entitiesUsed) {
const actionRoute = makeOperationRoute(relativeActionRoute)
async function internalAction(args, specificOptimisticUpdateDefinitions) {
registerActionInProgress(specificOptimisticUpdateDefinitions)
try {

View File

@ -1,10 +1,12 @@
import { useQuery } from '../queries'
import api, { handleApiError } from '../api'
import { HttpMethod } from '../types'
export default function useAuth(queryFnArgs, config) {
return useQuery(getMe, queryFnArgs, config)
}
async function getMe() {
export async function getMe() {
try {
const response = await api.get('/auth/me')
@ -19,3 +21,4 @@ async function getMe() {
}
getMe.queryCacheKey = ['auth/me']
getMe.route = { method: HttpMethod.Get, path: '/auth/me' }

View File

@ -1,11 +0,0 @@
import api, { handleApiError } from '../api'
export async function callOperation(operationRoute, args) {
try {
const response = await api.post(`/${operationRoute}`, args)
return response.data
} catch (error) {
handleApiError(error)
}
}

View File

@ -0,0 +1,17 @@
import api, { handleApiError } from '../api'
import { HttpMethod } from '../types'
export type OperationRoute = { method: HttpMethod, path: string }
export async function callOperation(operationRoute: OperationRoute & { method: HttpMethod.Post }, args: any) {
try {
const response = await api.post(operationRoute.path, args)
return response.data
} catch (error) {
handleApiError(error)
}
}
export function makeOperationRoute(relativeOperationRoute: string): OperationRoute {
return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` }
}

View File

@ -1,3 +1,3 @@
export type Query<Input, Output> = (args: Input) => Promise<Output>
import { type Query } from './index'
export function createQuery<Input, Output>(queryRoute: string, entitiesUsed: any[]): Query<Input, Output>

View File

@ -1,10 +1,12 @@
import { callOperation } from '../operations'
import { callOperation, makeOperationRoute } from '../operations'
import {
addResourcesUsedByQuery,
getActiveOptimisticUpdates,
} from '../operations/resources'
export function createQuery(queryRoute, entitiesUsed) {
export function createQuery(relativeQueryRoute, entitiesUsed) {
const queryRoute = makeOperationRoute(relativeQueryRoute)
async function query(queryKey, queryArgs) {
const serverResult = await callOperation(queryRoute, queryArgs)
return getActiveOptimisticUpdates(queryKey).reduce(
@ -13,7 +15,8 @@ export function createQuery(queryRoute, entitiesUsed) {
)
}
query.queryCacheKey = [queryRoute]
query.queryCacheKey = [relativeQueryRoute]
query.route = queryRoute
addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed)
return query

View File

@ -1,6 +1,12 @@
import { UseQueryResult } from "@tanstack/react-query";
export type Query<Input, Output> = (args: Input) => Promise<Output>
import { type HttpMethod } from "../types";
export type Query<Input, Output> = {
(args: Input): Promise<Output>
queryCacheKey: string[]
route: { method: HttpMethod, path: string }
}
export function useQuery<Input, Output, Error = unknown>(
queryFn: Query<Input, Output>,

View File

@ -0,0 +1 @@
export { renderInContext, mockServer } from './vitest/helpers'

View File

@ -0,0 +1,67 @@
import { ReactElement } from 'react'
import { rest, type ResponseResolver, type RestContext } from 'msw'
import { setupServer, type SetupServer } from 'msw/node'
import { BrowserRouter as Router } from 'react-router-dom'
import { render, RenderResult, cleanup } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { beforeAll, afterEach, afterAll } from 'vitest'
import { Query } from '../../queries'
import config from '../../config'
import { HttpMethod } from '../../types'
// Inspired by the Tanstack React Query helper:
// https://github.com/TanStack/query/blob/4ae99561ca3383d6de3f4aad656a49ba4a17b57a/packages/react-query/src/__tests__/utils.tsx#L7-L26
export function renderInContext(ui: ReactElement): RenderResult {
const client = new QueryClient()
const { rerender, ...result } = render(
<QueryClientProvider client={client}><Router>{ui}</Router></QueryClientProvider>
)
return {
...result,
rerender: (rerenderUi: ReactElement) =>
rerender(
<QueryClientProvider client={client}><Router>{rerenderUi}</Router></QueryClientProvider>
)
}
}
type QueryRoute = Query<any, any>['route']
export function mockServer(): {
server: SetupServer,
mockQuery: ({ route }: { route: QueryRoute }, resJson: any) => void
} {
const server: SetupServer = setupServer()
beforeAll(() => server.listen())
afterEach(() => {
server.resetHandlers()
cleanup()
})
afterAll(() => server.close())
function mockQuery({ route }: { route: QueryRoute }, resJson: any): void {
if (!Object.values(HttpMethod).includes(route.method)) {
throw new Error(`Unsupported query method for mocking: ${route.method}. Supported method strings are: ${Object.values(HttpMethod).join(', ')}.`)
}
const url = `${config.apiUrl}${route.path}`
const responseHandler: ResponseResolver<any, RestContext, any> = (_req, res, ctx) => {
return res(ctx.json(resJson))
}
// NOTE: Technically, we only need to care about POST for Queries
// and GET for the /auth/me route. However, an additional use case
// for this function could be to mock APIs, so more methods are supported.
const handlers: Record<HttpMethod, Parameters<typeof server.use>[0]> = {
[HttpMethod.Get]: rest.get(url, responseHandler),
[HttpMethod.Post]: rest.post(url, responseHandler),
[HttpMethod.Put]: rest.put(url, responseHandler),
[HttpMethod.Delete]: rest.delete(url, responseHandler),
}
server.use(handlers[route.method])
}
return { server, mockQuery }
}

View File

@ -0,0 +1,4 @@
import matchers from '@testing-library/jest-dom/matchers'
import { expect } from 'vitest'
expect.extend(matchers)

View File

@ -0,0 +1,7 @@
// NOTE: This is enough to cover Operations and our APIs (src/Wasp/AppSpec/Api.hs).
export enum HttpMethod {
Get = 'GET',
Post = 'POST',
Put = 'PUT',
Delete = 'DELETE',
}

View File

@ -1,3 +1,4 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
@ -13,4 +14,8 @@ export default defineConfig({
build: {
outDir: 'build',
},
test: {
environment: 'jsdom',
setupFiles: ['./src/test/vitest/setup.ts'],
},
})

View File

@ -53,7 +53,7 @@ waspBuild/.wasp/build/web-app/src/ext-src/vite-env.d.ts
waspBuild/.wasp/build/web-app/src/ext-src/waspLogo.png
waspBuild/.wasp/build/web-app/src/index.tsx
waspBuild/.wasp/build/web-app/src/logo.png
waspBuild/.wasp/build/web-app/src/operations/index.js
waspBuild/.wasp/build/web-app/src/operations/index.ts
waspBuild/.wasp/build/web-app/src/operations/resources.js
waspBuild/.wasp/build/web-app/src/operations/updateHandlersMap.js
waspBuild/.wasp/build/web-app/src/queries/core.d.ts
@ -63,6 +63,10 @@ waspBuild/.wasp/build/web-app/src/queries/index.js
waspBuild/.wasp/build/web-app/src/queryClient.js
waspBuild/.wasp/build/web-app/src/router.jsx
waspBuild/.wasp/build/web-app/src/storage.ts
waspBuild/.wasp/build/web-app/src/test/index.ts
waspBuild/.wasp/build/web-app/src/test/vitest/helpers.tsx
waspBuild/.wasp/build/web-app/src/test/vitest/setup.ts
waspBuild/.wasp/build/web-app/src/types.ts
waspBuild/.wasp/build/web-app/src/universal/url.ts
waspBuild/.wasp/build/web-app/src/utils.js
waspBuild/.wasp/build/web-app/src/vite-env.d.ts

View File

@ -270,7 +270,7 @@
"file",
"web-app/package.json"
],
"baa475d963b525435bc9fe392679d8750815f6a4477d221250440c29086bd6cc"
"2ade914fd6495047faa08ea4b70dcda24c4506207fcb45595734d154656daef7"
],
[
[
@ -312,7 +312,7 @@
"file",
"web-app/src/actions/core.js"
],
"5c4dcdec74fb014a8edbb3d240bcbbfc829e201bce64132598b444db14a2bd45"
"f38003d51d9754952bf595a25fdb44580d88af3bcd6658848cf8a339a8240689"
],
[
[
@ -387,9 +387,9 @@
[
[
"file",
"web-app/src/operations/index.js"
"web-app/src/operations/index.ts"
],
"0d41d5791489c43814dfcfae09cf98a44418de9c36393cece15a2ff22da7d0b2"
"b48c8c12e212509e33482c3408b21caa180ca96df243925fb1aaa80bdfd6a734"
],
[
[
@ -410,21 +410,21 @@
"file",
"web-app/src/queries/core.d.ts"
],
"f0b289140e92738451be386ca73a2fd1c84e9951eb2f1b9c6c09dfa3079d0c74"
"a5bcc60f2a082359e537e1a1f231bd66abcdf4d7066531e68fd2aede3c04c059"
],
[
[
"file",
"web-app/src/queries/core.js"
],
"2daf5b414722204281d65e954ce862a6fc586e8907b202800694909d23957c5e"
"a0ba3524c8efff27abce03406ba661ac56197ddecbe665cd53a41409c6e4fc5d"
],
[
[
"file",
"web-app/src/queries/index.d.ts"
],
"efc70de9916a60e19e0c86aaf955b0be0c999ba5c30139c3b6b98bcc4d382091"
"e6b8b66d31eee3284c20085a4def388e4a4f34eb945e7cf8f2cccd2b6c3cbc2d"
],
[
[
@ -454,6 +454,34 @@
],
"1e35eb73e486c8f926337a8c8ddfc392639de3718bf28fdc3073b0ca97c864f7"
],
[
[
"file",
"web-app/src/test/index.ts"
],
"cb2e2dc33df8afc0d4453f4322a4e2af92f3345e9622e0416fa87e34d6acb9d8"
],
[
[
"file",
"web-app/src/test/vitest/helpers.tsx"
],
"f79b242670c09642684014d4eb0c7106a1b42cc3efce73f5b2f9b551854ce513"
],
[
[
"file",
"web-app/src/test/vitest/setup.ts"
],
"1c08b10e428cec3939e0ab269c9a02694e196de7c5dd9f18372424bdccbc5028"
],
[
[
"file",
"web-app/src/types.ts"
],
"3be2f95a2d7d2fa790e6a077edc241bd26db5e167ad3a4033a88bbb0262bb1c6"
],
[
[
"file",
@ -494,6 +522,6 @@
"file",
"web-app/vite.config.ts"
],
"0f62618ba1d03ad7fca703b5fadc64e1a4be7c553f121dbefe8e1f7d6efe2a19"
"0ab8b3892a5d5d25b85646ef30e8b2487904415021912e68670fab316b2ecf2d"
]
]

View File

@ -1 +1 @@
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.5.0"}],"devDependencies":[{"name":"vite","version":"^4.1.0"},{"name":"typescript","version":"^4.9.3"},{"name":"@types/react","version":"^17.0.53"},{"name":"@types/react-dom","version":"^17.0.19"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^1.0.1"}]}}
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.5.0"}],"devDependencies":[{"name":"vite","version":"^4.1.0"},{"name":"typescript","version":"^4.9.3"},{"name":"@types/react","version":"^17.0.53"},{"name":"@types/react-dom","version":"^17.0.19"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^1.0.1"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^12.1.5"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}

View File

@ -20,14 +20,20 @@
"react-router-dom": "^5.3.3"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5",
"@tsconfig/vite-react": "^1.0.1",
"@types/react": "^17.0.53",
"@types/react-dom": "^17.0.19",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react-swc": "^3.0.0",
"@vitest/ui": "^0.29.3",
"dotenv": "^16.0.3",
"jsdom": "^21.1.1",
"msw": "^1.1.0",
"typescript": "^4.9.3",
"vite": "^4.1.0"
"vite": "^4.1.0",
"vitest": "^0.29.3"
},
"engineStrict": true,
"engines": {

View File

@ -1,10 +1,12 @@
import { callOperation } from '../operations'
import { callOperation, makeOperationRoute } from '../operations'
import {
registerActionInProgress,
registerActionDone,
} from '../operations/resources'
export function createAction(actionRoute, entitiesUsed) {
export function createAction(relativeActionRoute, entitiesUsed) {
const actionRoute = makeOperationRoute(relativeActionRoute)
async function internalAction(args, specificOptimisticUpdateDefinitions) {
registerActionInProgress(specificOptimisticUpdateDefinitions)
try {

View File

@ -1,11 +0,0 @@
import api, { handleApiError } from '../api'
export async function callOperation(operationRoute, args) {
try {
const response = await api.post(`/${operationRoute}`, args)
return response.data
} catch (error) {
handleApiError(error)
}
}

View File

@ -0,0 +1,17 @@
import api, { handleApiError } from '../api'
import { HttpMethod } from '../types'
export type OperationRoute = { method: HttpMethod, path: string }
export async function callOperation(operationRoute: OperationRoute & { method: HttpMethod.Post }, args: any) {
try {
const response = await api.post(operationRoute.path, args)
return response.data
} catch (error) {
handleApiError(error)
}
}
export function makeOperationRoute(relativeOperationRoute: string): OperationRoute {
return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` }
}

View File

@ -1,3 +1,3 @@
export type Query<Input, Output> = (args: Input) => Promise<Output>
import { type Query } from './index'
export function createQuery<Input, Output>(queryRoute: string, entitiesUsed: any[]): Query<Input, Output>

View File

@ -1,10 +1,12 @@
import { callOperation } from '../operations'
import { callOperation, makeOperationRoute } from '../operations'
import {
addResourcesUsedByQuery,
getActiveOptimisticUpdates,
} from '../operations/resources'
export function createQuery(queryRoute, entitiesUsed) {
export function createQuery(relativeQueryRoute, entitiesUsed) {
const queryRoute = makeOperationRoute(relativeQueryRoute)
async function query(queryKey, queryArgs) {
const serverResult = await callOperation(queryRoute, queryArgs)
return getActiveOptimisticUpdates(queryKey).reduce(
@ -13,7 +15,8 @@ export function createQuery(queryRoute, entitiesUsed) {
)
}
query.queryCacheKey = [queryRoute]
query.queryCacheKey = [relativeQueryRoute]
query.route = queryRoute
addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed)
return query

View File

@ -1,6 +1,12 @@
import { UseQueryResult } from "@tanstack/react-query";
export type Query<Input, Output> = (args: Input) => Promise<Output>
import { type HttpMethod } from "../types";
export type Query<Input, Output> = {
(args: Input): Promise<Output>
queryCacheKey: string[]
route: { method: HttpMethod, path: string }
}
export function useQuery<Input, Output, Error = unknown>(
queryFn: Query<Input, Output>,

View File

@ -0,0 +1 @@
export { renderInContext, mockServer } from './vitest/helpers'

View File

@ -0,0 +1,67 @@
import { ReactElement } from 'react'
import { rest, type ResponseResolver, type RestContext } from 'msw'
import { setupServer, type SetupServer } from 'msw/node'
import { BrowserRouter as Router } from 'react-router-dom'
import { render, RenderResult, cleanup } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { beforeAll, afterEach, afterAll } from 'vitest'
import { Query } from '../../queries'
import config from '../../config'
import { HttpMethod } from '../../types'
// Inspired by the Tanstack React Query helper:
// https://github.com/TanStack/query/blob/4ae99561ca3383d6de3f4aad656a49ba4a17b57a/packages/react-query/src/__tests__/utils.tsx#L7-L26
export function renderInContext(ui: ReactElement): RenderResult {
const client = new QueryClient()
const { rerender, ...result } = render(
<QueryClientProvider client={client}><Router>{ui}</Router></QueryClientProvider>
)
return {
...result,
rerender: (rerenderUi: ReactElement) =>
rerender(
<QueryClientProvider client={client}><Router>{rerenderUi}</Router></QueryClientProvider>
)
}
}
type QueryRoute = Query<any, any>['route']
export function mockServer(): {
server: SetupServer,
mockQuery: ({ route }: { route: QueryRoute }, resJson: any) => void
} {
const server: SetupServer = setupServer()
beforeAll(() => server.listen())
afterEach(() => {
server.resetHandlers()
cleanup()
})
afterAll(() => server.close())
function mockQuery({ route }: { route: QueryRoute }, resJson: any): void {
if (!Object.values(HttpMethod).includes(route.method)) {
throw new Error(`Unsupported query method for mocking: ${route.method}. Supported method strings are: ${Object.values(HttpMethod).join(', ')}.`)
}
const url = `${config.apiUrl}${route.path}`
const responseHandler: ResponseResolver<any, RestContext, any> = (_req, res, ctx) => {
return res(ctx.json(resJson))
}
// NOTE: Technically, we only need to care about POST for Queries
// and GET for the /auth/me route. However, an additional use case
// for this function could be to mock APIs, so more methods are supported.
const handlers: Record<HttpMethod, Parameters<typeof server.use>[0]> = {
[HttpMethod.Get]: rest.get(url, responseHandler),
[HttpMethod.Post]: rest.post(url, responseHandler),
[HttpMethod.Put]: rest.put(url, responseHandler),
[HttpMethod.Delete]: rest.delete(url, responseHandler),
}
server.use(handlers[route.method])
}
return { server, mockQuery }
}

View File

@ -0,0 +1,4 @@
import matchers from '@testing-library/jest-dom/matchers'
import { expect } from 'vitest'
expect.extend(matchers)

View File

@ -0,0 +1,7 @@
// NOTE: This is enough to cover Operations and our APIs (src/Wasp/AppSpec/Api.hs).
export enum HttpMethod {
Get = 'GET',
Post = 'POST',
Put = 'PUT',
Delete = 'DELETE',
}

View File

@ -1,3 +1,4 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
@ -13,4 +14,8 @@ export default defineConfig({
build: {
outDir: 'build',
},
test: {
environment: 'jsdom',
setupFiles: ['./src/test/vitest/setup.ts'],
},
})

View File

@ -55,7 +55,7 @@ waspCompile/.wasp/out/web-app/src/ext-src/vite-env.d.ts
waspCompile/.wasp/out/web-app/src/ext-src/waspLogo.png
waspCompile/.wasp/out/web-app/src/index.tsx
waspCompile/.wasp/out/web-app/src/logo.png
waspCompile/.wasp/out/web-app/src/operations/index.js
waspCompile/.wasp/out/web-app/src/operations/index.ts
waspCompile/.wasp/out/web-app/src/operations/resources.js
waspCompile/.wasp/out/web-app/src/operations/updateHandlersMap.js
waspCompile/.wasp/out/web-app/src/queries/core.d.ts
@ -65,6 +65,10 @@ waspCompile/.wasp/out/web-app/src/queries/index.js
waspCompile/.wasp/out/web-app/src/queryClient.js
waspCompile/.wasp/out/web-app/src/router.jsx
waspCompile/.wasp/out/web-app/src/storage.ts
waspCompile/.wasp/out/web-app/src/test/index.ts
waspCompile/.wasp/out/web-app/src/test/vitest/helpers.tsx
waspCompile/.wasp/out/web-app/src/test/vitest/setup.ts
waspCompile/.wasp/out/web-app/src/types.ts
waspCompile/.wasp/out/web-app/src/universal/url.ts
waspCompile/.wasp/out/web-app/src/utils.js
waspCompile/.wasp/out/web-app/src/vite-env.d.ts

View File

@ -284,7 +284,7 @@
"file",
"web-app/package.json"
],
"82391614a4b461fddbfe2fb9945e612be5808c71f10b870297c295ea0abc465e"
"898210f01bc26ce44f1542ec9e5c28e00779a0192d5a7af329060bdbc76eec8e"
],
[
[
@ -326,7 +326,7 @@
"file",
"web-app/src/actions/core.js"
],
"5c4dcdec74fb014a8edbb3d240bcbbfc829e201bce64132598b444db14a2bd45"
"f38003d51d9754952bf595a25fdb44580d88af3bcd6658848cf8a339a8240689"
],
[
[
@ -401,9 +401,9 @@
[
[
"file",
"web-app/src/operations/index.js"
"web-app/src/operations/index.ts"
],
"0d41d5791489c43814dfcfae09cf98a44418de9c36393cece15a2ff22da7d0b2"
"b48c8c12e212509e33482c3408b21caa180ca96df243925fb1aaa80bdfd6a734"
],
[
[
@ -424,21 +424,21 @@
"file",
"web-app/src/queries/core.d.ts"
],
"f0b289140e92738451be386ca73a2fd1c84e9951eb2f1b9c6c09dfa3079d0c74"
"a5bcc60f2a082359e537e1a1f231bd66abcdf4d7066531e68fd2aede3c04c059"
],
[
[
"file",
"web-app/src/queries/core.js"
],
"2daf5b414722204281d65e954ce862a6fc586e8907b202800694909d23957c5e"
"a0ba3524c8efff27abce03406ba661ac56197ddecbe665cd53a41409c6e4fc5d"
],
[
[
"file",
"web-app/src/queries/index.d.ts"
],
"efc70de9916a60e19e0c86aaf955b0be0c999ba5c30139c3b6b98bcc4d382091"
"e6b8b66d31eee3284c20085a4def388e4a4f34eb945e7cf8f2cccd2b6c3cbc2d"
],
[
[
@ -468,6 +468,34 @@
],
"1e35eb73e486c8f926337a8c8ddfc392639de3718bf28fdc3073b0ca97c864f7"
],
[
[
"file",
"web-app/src/test/index.ts"
],
"cb2e2dc33df8afc0d4453f4322a4e2af92f3345e9622e0416fa87e34d6acb9d8"
],
[
[
"file",
"web-app/src/test/vitest/helpers.tsx"
],
"f79b242670c09642684014d4eb0c7106a1b42cc3efce73f5b2f9b551854ce513"
],
[
[
"file",
"web-app/src/test/vitest/setup.ts"
],
"1c08b10e428cec3939e0ab269c9a02694e196de7c5dd9f18372424bdccbc5028"
],
[
[
"file",
"web-app/src/types.ts"
],
"3be2f95a2d7d2fa790e6a077edc241bd26db5e167ad3a4033a88bbb0262bb1c6"
],
[
[
"file",
@ -508,6 +536,6 @@
"file",
"web-app/vite.config.ts"
],
"0f62618ba1d03ad7fca703b5fadc64e1a4be7c553f121dbefe8e1f7d6efe2a19"
"0ab8b3892a5d5d25b85646ef30e8b2487904415021912e68670fab316b2ecf2d"
]
]

View File

@ -1 +1 @@
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.5.0"}],"devDependencies":[{"name":"vite","version":"^4.1.0"},{"name":"typescript","version":"^4.9.3"},{"name":"@types/react","version":"^17.0.53"},{"name":"@types/react-dom","version":"^17.0.19"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^1.0.1"}]}}
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.5.0"}],"devDependencies":[{"name":"vite","version":"^4.1.0"},{"name":"typescript","version":"^4.9.3"},{"name":"@types/react","version":"^17.0.53"},{"name":"@types/react-dom","version":"^17.0.19"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^1.0.1"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^12.1.5"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}

View File

@ -20,14 +20,20 @@
"react-router-dom": "^5.3.3"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5",
"@tsconfig/vite-react": "^1.0.1",
"@types/react": "^17.0.53",
"@types/react-dom": "^17.0.19",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react-swc": "^3.0.0",
"@vitest/ui": "^0.29.3",
"dotenv": "^16.0.3",
"jsdom": "^21.1.1",
"msw": "^1.1.0",
"typescript": "^4.9.3",
"vite": "^4.1.0"
"vite": "^4.1.0",
"vitest": "^0.29.3"
},
"engineStrict": true,
"engines": {

View File

@ -1,10 +1,12 @@
import { callOperation } from '../operations'
import { callOperation, makeOperationRoute } from '../operations'
import {
registerActionInProgress,
registerActionDone,
} from '../operations/resources'
export function createAction(actionRoute, entitiesUsed) {
export function createAction(relativeActionRoute, entitiesUsed) {
const actionRoute = makeOperationRoute(relativeActionRoute)
async function internalAction(args, specificOptimisticUpdateDefinitions) {
registerActionInProgress(specificOptimisticUpdateDefinitions)
try {

View File

@ -1,11 +0,0 @@
import api, { handleApiError } from '../api'
export async function callOperation(operationRoute, args) {
try {
const response = await api.post(`/${operationRoute}`, args)
return response.data
} catch (error) {
handleApiError(error)
}
}

View File

@ -0,0 +1,17 @@
import api, { handleApiError } from '../api'
import { HttpMethod } from '../types'
export type OperationRoute = { method: HttpMethod, path: string }
export async function callOperation(operationRoute: OperationRoute & { method: HttpMethod.Post }, args: any) {
try {
const response = await api.post(operationRoute.path, args)
return response.data
} catch (error) {
handleApiError(error)
}
}
export function makeOperationRoute(relativeOperationRoute: string): OperationRoute {
return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` }
}

View File

@ -1,3 +1,3 @@
export type Query<Input, Output> = (args: Input) => Promise<Output>
import { type Query } from './index'
export function createQuery<Input, Output>(queryRoute: string, entitiesUsed: any[]): Query<Input, Output>

View File

@ -1,10 +1,12 @@
import { callOperation } from '../operations'
import { callOperation, makeOperationRoute } from '../operations'
import {
addResourcesUsedByQuery,
getActiveOptimisticUpdates,
} from '../operations/resources'
export function createQuery(queryRoute, entitiesUsed) {
export function createQuery(relativeQueryRoute, entitiesUsed) {
const queryRoute = makeOperationRoute(relativeQueryRoute)
async function query(queryKey, queryArgs) {
const serverResult = await callOperation(queryRoute, queryArgs)
return getActiveOptimisticUpdates(queryKey).reduce(
@ -13,7 +15,8 @@ export function createQuery(queryRoute, entitiesUsed) {
)
}
query.queryCacheKey = [queryRoute]
query.queryCacheKey = [relativeQueryRoute]
query.route = queryRoute
addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed)
return query

View File

@ -1,6 +1,12 @@
import { UseQueryResult } from "@tanstack/react-query";
export type Query<Input, Output> = (args: Input) => Promise<Output>
import { type HttpMethod } from "../types";
export type Query<Input, Output> = {
(args: Input): Promise<Output>
queryCacheKey: string[]
route: { method: HttpMethod, path: string }
}
export function useQuery<Input, Output, Error = unknown>(
queryFn: Query<Input, Output>,

View File

@ -0,0 +1 @@
export { renderInContext, mockServer } from './vitest/helpers'

View File

@ -0,0 +1,67 @@
import { ReactElement } from 'react'
import { rest, type ResponseResolver, type RestContext } from 'msw'
import { setupServer, type SetupServer } from 'msw/node'
import { BrowserRouter as Router } from 'react-router-dom'
import { render, RenderResult, cleanup } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { beforeAll, afterEach, afterAll } from 'vitest'
import { Query } from '../../queries'
import config from '../../config'
import { HttpMethod } from '../../types'
// Inspired by the Tanstack React Query helper:
// https://github.com/TanStack/query/blob/4ae99561ca3383d6de3f4aad656a49ba4a17b57a/packages/react-query/src/__tests__/utils.tsx#L7-L26
export function renderInContext(ui: ReactElement): RenderResult {
const client = new QueryClient()
const { rerender, ...result } = render(
<QueryClientProvider client={client}><Router>{ui}</Router></QueryClientProvider>
)
return {
...result,
rerender: (rerenderUi: ReactElement) =>
rerender(
<QueryClientProvider client={client}><Router>{rerenderUi}</Router></QueryClientProvider>
)
}
}
type QueryRoute = Query<any, any>['route']
export function mockServer(): {
server: SetupServer,
mockQuery: ({ route }: { route: QueryRoute }, resJson: any) => void
} {
const server: SetupServer = setupServer()
beforeAll(() => server.listen())
afterEach(() => {
server.resetHandlers()
cleanup()
})
afterAll(() => server.close())
function mockQuery({ route }: { route: QueryRoute }, resJson: any): void {
if (!Object.values(HttpMethod).includes(route.method)) {
throw new Error(`Unsupported query method for mocking: ${route.method}. Supported method strings are: ${Object.values(HttpMethod).join(', ')}.`)
}
const url = `${config.apiUrl}${route.path}`
const responseHandler: ResponseResolver<any, RestContext, any> = (_req, res, ctx) => {
return res(ctx.json(resJson))
}
// NOTE: Technically, we only need to care about POST for Queries
// and GET for the /auth/me route. However, an additional use case
// for this function could be to mock APIs, so more methods are supported.
const handlers: Record<HttpMethod, Parameters<typeof server.use>[0]> = {
[HttpMethod.Get]: rest.get(url, responseHandler),
[HttpMethod.Post]: rest.post(url, responseHandler),
[HttpMethod.Put]: rest.put(url, responseHandler),
[HttpMethod.Delete]: rest.delete(url, responseHandler),
}
server.use(handlers[route.method])
}
return { server, mockQuery }
}

View File

@ -0,0 +1,4 @@
import matchers from '@testing-library/jest-dom/matchers'
import { expect } from 'vitest'
expect.extend(matchers)

View File

@ -0,0 +1,7 @@
// NOTE: This is enough to cover Operations and our APIs (src/Wasp/AppSpec/Api.hs).
export enum HttpMethod {
Get = 'GET',
Post = 'POST',
Put = 'PUT',
Delete = 'DELETE',
}

View File

@ -1,3 +1,4 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
@ -13,4 +14,8 @@ export default defineConfig({
build: {
outDir: 'build',
},
test: {
environment: 'jsdom',
setupFiles: ['./src/test/vitest/setup.ts'],
},
})

View File

@ -99,7 +99,7 @@ waspComplexTest/.wasp/out/web-app/src/ext-src/vite-env.d.ts
waspComplexTest/.wasp/out/web-app/src/ext-src/waspLogo.png
waspComplexTest/.wasp/out/web-app/src/index.tsx
waspComplexTest/.wasp/out/web-app/src/logo.png
waspComplexTest/.wasp/out/web-app/src/operations/index.js
waspComplexTest/.wasp/out/web-app/src/operations/index.ts
waspComplexTest/.wasp/out/web-app/src/operations/resources.js
waspComplexTest/.wasp/out/web-app/src/operations/updateHandlersMap.js
waspComplexTest/.wasp/out/web-app/src/queries/MySpecialQuery.js
@ -110,6 +110,10 @@ waspComplexTest/.wasp/out/web-app/src/queries/index.js
waspComplexTest/.wasp/out/web-app/src/queryClient.js
waspComplexTest/.wasp/out/web-app/src/router.jsx
waspComplexTest/.wasp/out/web-app/src/storage.ts
waspComplexTest/.wasp/out/web-app/src/test/index.ts
waspComplexTest/.wasp/out/web-app/src/test/vitest/helpers.tsx
waspComplexTest/.wasp/out/web-app/src/test/vitest/setup.ts
waspComplexTest/.wasp/out/web-app/src/types.ts
waspComplexTest/.wasp/out/web-app/src/universal/url.ts
waspComplexTest/.wasp/out/web-app/src/utils.js
waspComplexTest/.wasp/out/web-app/src/vite-env.d.ts

View File

@ -501,7 +501,7 @@
"file",
"web-app/package.json"
],
"7076213d4eff673d37d271baa15cdda5cd5a647d31944015f52ef72aad558ac8"
"bbb4abefcbfeb37e6feaee0906b849939df6bd85d70821a81217be6cff2c6b1d"
],
[
[
@ -557,7 +557,7 @@
"file",
"web-app/src/actions/core.js"
],
"5c4dcdec74fb014a8edbb3d240bcbbfc829e201bce64132598b444db14a2bd45"
"f38003d51d9754952bf595a25fdb44580d88af3bcd6658848cf8a339a8240689"
],
[
[
@ -613,7 +613,7 @@
"file",
"web-app/src/auth/useAuth.js"
],
"ce3d41effe82eb317557cbdd3296174ff0e4d5acff6aa804a396b0cf453f95e6"
"04dcb92980f2d533ca281cb70ff06dd41744e4158a07bf8e599537d13ce4af91"
],
[
[
@ -688,9 +688,9 @@
[
[
"file",
"web-app/src/operations/index.js"
"web-app/src/operations/index.ts"
],
"0d41d5791489c43814dfcfae09cf98a44418de9c36393cece15a2ff22da7d0b2"
"b48c8c12e212509e33482c3408b21caa180ca96df243925fb1aaa80bdfd6a734"
],
[
[
@ -718,21 +718,21 @@
"file",
"web-app/src/queries/core.d.ts"
],
"f0b289140e92738451be386ca73a2fd1c84e9951eb2f1b9c6c09dfa3079d0c74"
"a5bcc60f2a082359e537e1a1f231bd66abcdf4d7066531e68fd2aede3c04c059"
],
[
[
"file",
"web-app/src/queries/core.js"
],
"2daf5b414722204281d65e954ce862a6fc586e8907b202800694909d23957c5e"
"a0ba3524c8efff27abce03406ba661ac56197ddecbe665cd53a41409c6e4fc5d"
],
[
[
"file",
"web-app/src/queries/index.d.ts"
],
"efc70de9916a60e19e0c86aaf955b0be0c999ba5c30139c3b6b98bcc4d382091"
"e6b8b66d31eee3284c20085a4def388e4a4f34eb945e7cf8f2cccd2b6c3cbc2d"
],
[
[
@ -762,6 +762,34 @@
],
"1e35eb73e486c8f926337a8c8ddfc392639de3718bf28fdc3073b0ca97c864f7"
],
[
[
"file",
"web-app/src/test/index.ts"
],
"cb2e2dc33df8afc0d4453f4322a4e2af92f3345e9622e0416fa87e34d6acb9d8"
],
[
[
"file",
"web-app/src/test/vitest/helpers.tsx"
],
"f79b242670c09642684014d4eb0c7106a1b42cc3efce73f5b2f9b551854ce513"
],
[
[
"file",
"web-app/src/test/vitest/setup.ts"
],
"1c08b10e428cec3939e0ab269c9a02694e196de7c5dd9f18372424bdccbc5028"
],
[
[
"file",
"web-app/src/types.ts"
],
"3be2f95a2d7d2fa790e6a077edc241bd26db5e167ad3a4033a88bbb0262bb1c6"
],
[
[
"file",
@ -802,6 +830,6 @@
"file",
"web-app/vite.config.ts"
],
"0f62618ba1d03ad7fca703b5fadc64e1a4be7c553f121dbefe8e1f7d6efe2a19"
"0ab8b3892a5d5d25b85646ef30e8b2487904415021912e68670fab316b2ecf2d"
]
]

View File

@ -1 +1 @@
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"passport","version":"0.6.0"},{"name":"passport-google-oauth20","version":"2.0.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"@sendgrid/mail","version":"^7.7.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.5.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"vite","version":"^4.1.0"},{"name":"typescript","version":"^4.9.3"},{"name":"@types/react","version":"^17.0.53"},{"name":"@types/react-dom","version":"^17.0.19"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^1.0.1"}]}}
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"passport","version":"0.6.0"},{"name":"passport-google-oauth20","version":"2.0.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"@sendgrid/mail","version":"^7.7.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.5.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"vite","version":"^4.1.0"},{"name":"typescript","version":"^4.9.3"},{"name":"@types/react","version":"^17.0.53"},{"name":"@types/react-dom","version":"^17.0.19"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^1.0.1"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^12.1.5"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}

View File

@ -22,14 +22,20 @@
"redux": "^4.0.5"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5",
"@tsconfig/vite-react": "^1.0.1",
"@types/react": "^17.0.53",
"@types/react-dom": "^17.0.19",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react-swc": "^3.0.0",
"@vitest/ui": "^0.29.3",
"dotenv": "^16.0.3",
"jsdom": "^21.1.1",
"msw": "^1.1.0",
"typescript": "^4.9.3",
"vite": "^4.1.0"
"vite": "^4.1.0",
"vitest": "^0.29.3"
},
"engineStrict": true,
"engines": {

View File

@ -1,10 +1,12 @@
import { callOperation } from '../operations'
import { callOperation, makeOperationRoute } from '../operations'
import {
registerActionInProgress,
registerActionDone,
} from '../operations/resources'
export function createAction(actionRoute, entitiesUsed) {
export function createAction(relativeActionRoute, entitiesUsed) {
const actionRoute = makeOperationRoute(relativeActionRoute)
async function internalAction(args, specificOptimisticUpdateDefinitions) {
registerActionInProgress(specificOptimisticUpdateDefinitions)
try {

View File

@ -1,10 +1,12 @@
import { useQuery } from '../queries'
import api, { handleApiError } from '../api'
import { HttpMethod } from '../types'
export default function useAuth(queryFnArgs, config) {
return useQuery(getMe, queryFnArgs, config)
}
async function getMe() {
export async function getMe() {
try {
const response = await api.get('/auth/me')
@ -19,3 +21,4 @@ async function getMe() {
}
getMe.queryCacheKey = ['auth/me']
getMe.route = { method: HttpMethod.Get, path: '/auth/me' }

View File

@ -1,11 +0,0 @@
import api, { handleApiError } from '../api'
export async function callOperation(operationRoute, args) {
try {
const response = await api.post(`/${operationRoute}`, args)
return response.data
} catch (error) {
handleApiError(error)
}
}

View File

@ -0,0 +1,17 @@
import api, { handleApiError } from '../api'
import { HttpMethod } from '../types'
export type OperationRoute = { method: HttpMethod, path: string }
export async function callOperation(operationRoute: OperationRoute & { method: HttpMethod.Post }, args: any) {
try {
const response = await api.post(operationRoute.path, args)
return response.data
} catch (error) {
handleApiError(error)
}
}
export function makeOperationRoute(relativeOperationRoute: string): OperationRoute {
return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` }
}

View File

@ -1,3 +1,3 @@
export type Query<Input, Output> = (args: Input) => Promise<Output>
import { type Query } from './index'
export function createQuery<Input, Output>(queryRoute: string, entitiesUsed: any[]): Query<Input, Output>

View File

@ -1,10 +1,12 @@
import { callOperation } from '../operations'
import { callOperation, makeOperationRoute } from '../operations'
import {
addResourcesUsedByQuery,
getActiveOptimisticUpdates,
} from '../operations/resources'
export function createQuery(queryRoute, entitiesUsed) {
export function createQuery(relativeQueryRoute, entitiesUsed) {
const queryRoute = makeOperationRoute(relativeQueryRoute)
async function query(queryKey, queryArgs) {
const serverResult = await callOperation(queryRoute, queryArgs)
return getActiveOptimisticUpdates(queryKey).reduce(
@ -13,7 +15,8 @@ export function createQuery(queryRoute, entitiesUsed) {
)
}
query.queryCacheKey = [queryRoute]
query.queryCacheKey = [relativeQueryRoute]
query.route = queryRoute
addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed)
return query

View File

@ -1,6 +1,12 @@
import { UseQueryResult } from "@tanstack/react-query";
export type Query<Input, Output> = (args: Input) => Promise<Output>
import { type HttpMethod } from "../types";
export type Query<Input, Output> = {
(args: Input): Promise<Output>
queryCacheKey: string[]
route: { method: HttpMethod, path: string }
}
export function useQuery<Input, Output, Error = unknown>(
queryFn: Query<Input, Output>,

View File

@ -0,0 +1 @@
export { renderInContext, mockServer } from './vitest/helpers'

View File

@ -0,0 +1,67 @@
import { ReactElement } from 'react'
import { rest, type ResponseResolver, type RestContext } from 'msw'
import { setupServer, type SetupServer } from 'msw/node'
import { BrowserRouter as Router } from 'react-router-dom'
import { render, RenderResult, cleanup } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { beforeAll, afterEach, afterAll } from 'vitest'
import { Query } from '../../queries'
import config from '../../config'
import { HttpMethod } from '../../types'
// Inspired by the Tanstack React Query helper:
// https://github.com/TanStack/query/blob/4ae99561ca3383d6de3f4aad656a49ba4a17b57a/packages/react-query/src/__tests__/utils.tsx#L7-L26
export function renderInContext(ui: ReactElement): RenderResult {
const client = new QueryClient()
const { rerender, ...result } = render(
<QueryClientProvider client={client}><Router>{ui}</Router></QueryClientProvider>
)
return {
...result,
rerender: (rerenderUi: ReactElement) =>
rerender(
<QueryClientProvider client={client}><Router>{rerenderUi}</Router></QueryClientProvider>
)
}
}
type QueryRoute = Query<any, any>['route']
export function mockServer(): {
server: SetupServer,
mockQuery: ({ route }: { route: QueryRoute }, resJson: any) => void
} {
const server: SetupServer = setupServer()
beforeAll(() => server.listen())
afterEach(() => {
server.resetHandlers()
cleanup()
})
afterAll(() => server.close())
function mockQuery({ route }: { route: QueryRoute }, resJson: any): void {
if (!Object.values(HttpMethod).includes(route.method)) {
throw new Error(`Unsupported query method for mocking: ${route.method}. Supported method strings are: ${Object.values(HttpMethod).join(', ')}.`)
}
const url = `${config.apiUrl}${route.path}`
const responseHandler: ResponseResolver<any, RestContext, any> = (_req, res, ctx) => {
return res(ctx.json(resJson))
}
// NOTE: Technically, we only need to care about POST for Queries
// and GET for the /auth/me route. However, an additional use case
// for this function could be to mock APIs, so more methods are supported.
const handlers: Record<HttpMethod, Parameters<typeof server.use>[0]> = {
[HttpMethod.Get]: rest.get(url, responseHandler),
[HttpMethod.Post]: rest.post(url, responseHandler),
[HttpMethod.Put]: rest.put(url, responseHandler),
[HttpMethod.Delete]: rest.delete(url, responseHandler),
}
server.use(handlers[route.method])
}
return { server, mockQuery }
}

View File

@ -0,0 +1,4 @@
import matchers from '@testing-library/jest-dom/matchers'
import { expect } from 'vitest'
expect.extend(matchers)

View File

@ -0,0 +1,7 @@
// NOTE: This is enough to cover Operations and our APIs (src/Wasp/AppSpec/Api.hs).
export enum HttpMethod {
Get = 'GET',
Post = 'POST',
Put = 'PUT',
Delete = 'DELETE',
}

View File

@ -1,3 +1,4 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
@ -13,4 +14,8 @@ export default defineConfig({
build: {
outDir: 'build',
},
test: {
environment: 'jsdom',
setupFiles: ['./src/test/vitest/setup.ts'],
},
})

View File

@ -58,7 +58,7 @@ waspJob/.wasp/out/web-app/src/ext-src/vite-env.d.ts
waspJob/.wasp/out/web-app/src/ext-src/waspLogo.png
waspJob/.wasp/out/web-app/src/index.tsx
waspJob/.wasp/out/web-app/src/logo.png
waspJob/.wasp/out/web-app/src/operations/index.js
waspJob/.wasp/out/web-app/src/operations/index.ts
waspJob/.wasp/out/web-app/src/operations/resources.js
waspJob/.wasp/out/web-app/src/operations/updateHandlersMap.js
waspJob/.wasp/out/web-app/src/queries/core.d.ts
@ -68,6 +68,10 @@ waspJob/.wasp/out/web-app/src/queries/index.js
waspJob/.wasp/out/web-app/src/queryClient.js
waspJob/.wasp/out/web-app/src/router.jsx
waspJob/.wasp/out/web-app/src/storage.ts
waspJob/.wasp/out/web-app/src/test/index.ts
waspJob/.wasp/out/web-app/src/test/vitest/helpers.tsx
waspJob/.wasp/out/web-app/src/test/vitest/setup.ts
waspJob/.wasp/out/web-app/src/types.ts
waspJob/.wasp/out/web-app/src/universal/url.ts
waspJob/.wasp/out/web-app/src/utils.js
waspJob/.wasp/out/web-app/src/vite-env.d.ts

View File

@ -298,7 +298,7 @@
"file",
"web-app/package.json"
],
"6f13980fb69f631e22c97fa3961f66182327564bc34669a2b86a5607df0eee21"
"776698634393dc6bcd56635765cfa98a63d6eb8a83a095b28b88f2e241e706ce"
],
[
[
@ -340,7 +340,7 @@
"file",
"web-app/src/actions/core.js"
],
"5c4dcdec74fb014a8edbb3d240bcbbfc829e201bce64132598b444db14a2bd45"
"f38003d51d9754952bf595a25fdb44580d88af3bcd6658848cf8a339a8240689"
],
[
[
@ -415,9 +415,9 @@
[
[
"file",
"web-app/src/operations/index.js"
"web-app/src/operations/index.ts"
],
"0d41d5791489c43814dfcfae09cf98a44418de9c36393cece15a2ff22da7d0b2"
"b48c8c12e212509e33482c3408b21caa180ca96df243925fb1aaa80bdfd6a734"
],
[
[
@ -438,21 +438,21 @@
"file",
"web-app/src/queries/core.d.ts"
],
"f0b289140e92738451be386ca73a2fd1c84e9951eb2f1b9c6c09dfa3079d0c74"
"a5bcc60f2a082359e537e1a1f231bd66abcdf4d7066531e68fd2aede3c04c059"
],
[
[
"file",
"web-app/src/queries/core.js"
],
"2daf5b414722204281d65e954ce862a6fc586e8907b202800694909d23957c5e"
"a0ba3524c8efff27abce03406ba661ac56197ddecbe665cd53a41409c6e4fc5d"
],
[
[
"file",
"web-app/src/queries/index.d.ts"
],
"efc70de9916a60e19e0c86aaf955b0be0c999ba5c30139c3b6b98bcc4d382091"
"e6b8b66d31eee3284c20085a4def388e4a4f34eb945e7cf8f2cccd2b6c3cbc2d"
],
[
[
@ -482,6 +482,34 @@
],
"1e35eb73e486c8f926337a8c8ddfc392639de3718bf28fdc3073b0ca97c864f7"
],
[
[
"file",
"web-app/src/test/index.ts"
],
"cb2e2dc33df8afc0d4453f4322a4e2af92f3345e9622e0416fa87e34d6acb9d8"
],
[
[
"file",
"web-app/src/test/vitest/helpers.tsx"
],
"f79b242670c09642684014d4eb0c7106a1b42cc3efce73f5b2f9b551854ce513"
],
[
[
"file",
"web-app/src/test/vitest/setup.ts"
],
"1c08b10e428cec3939e0ab269c9a02694e196de7c5dd9f18372424bdccbc5028"
],
[
[
"file",
"web-app/src/types.ts"
],
"3be2f95a2d7d2fa790e6a077edc241bd26db5e167ad3a4033a88bbb0262bb1c6"
],
[
[
"file",
@ -522,6 +550,6 @@
"file",
"web-app/vite.config.ts"
],
"0f62618ba1d03ad7fca703b5fadc64e1a4be7c553f121dbefe8e1f7d6efe2a19"
"0ab8b3892a5d5d25b85646ef30e8b2487904415021912e68670fab316b2ecf2d"
]
]

View File

@ -1 +1 @@
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"pg-boss","version":"^8.4.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.5.0"}],"devDependencies":[{"name":"vite","version":"^4.1.0"},{"name":"typescript","version":"^4.9.3"},{"name":"@types/react","version":"^17.0.53"},{"name":"@types/react-dom","version":"^17.0.19"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^1.0.1"}]}}
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"pg-boss","version":"^8.4.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.5.0"}],"devDependencies":[{"name":"vite","version":"^4.1.0"},{"name":"typescript","version":"^4.9.3"},{"name":"@types/react","version":"^17.0.53"},{"name":"@types/react-dom","version":"^17.0.19"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^1.0.1"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^12.1.5"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}

View File

@ -20,14 +20,20 @@
"react-router-dom": "^5.3.3"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5",
"@tsconfig/vite-react": "^1.0.1",
"@types/react": "^17.0.53",
"@types/react-dom": "^17.0.19",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react-swc": "^3.0.0",
"@vitest/ui": "^0.29.3",
"dotenv": "^16.0.3",
"jsdom": "^21.1.1",
"msw": "^1.1.0",
"typescript": "^4.9.3",
"vite": "^4.1.0"
"vite": "^4.1.0",
"vitest": "^0.29.3"
},
"engineStrict": true,
"engines": {

View File

@ -1,10 +1,12 @@
import { callOperation } from '../operations'
import { callOperation, makeOperationRoute } from '../operations'
import {
registerActionInProgress,
registerActionDone,
} from '../operations/resources'
export function createAction(actionRoute, entitiesUsed) {
export function createAction(relativeActionRoute, entitiesUsed) {
const actionRoute = makeOperationRoute(relativeActionRoute)
async function internalAction(args, specificOptimisticUpdateDefinitions) {
registerActionInProgress(specificOptimisticUpdateDefinitions)
try {

View File

@ -1,11 +0,0 @@
import api, { handleApiError } from '../api'
export async function callOperation(operationRoute, args) {
try {
const response = await api.post(`/${operationRoute}`, args)
return response.data
} catch (error) {
handleApiError(error)
}
}

View File

@ -0,0 +1,17 @@
import api, { handleApiError } from '../api'
import { HttpMethod } from '../types'
export type OperationRoute = { method: HttpMethod, path: string }
export async function callOperation(operationRoute: OperationRoute & { method: HttpMethod.Post }, args: any) {
try {
const response = await api.post(operationRoute.path, args)
return response.data
} catch (error) {
handleApiError(error)
}
}
export function makeOperationRoute(relativeOperationRoute: string): OperationRoute {
return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` }
}

View File

@ -1,3 +1,3 @@
export type Query<Input, Output> = (args: Input) => Promise<Output>
import { type Query } from './index'
export function createQuery<Input, Output>(queryRoute: string, entitiesUsed: any[]): Query<Input, Output>

View File

@ -1,10 +1,12 @@
import { callOperation } from '../operations'
import { callOperation, makeOperationRoute } from '../operations'
import {
addResourcesUsedByQuery,
getActiveOptimisticUpdates,
} from '../operations/resources'
export function createQuery(queryRoute, entitiesUsed) {
export function createQuery(relativeQueryRoute, entitiesUsed) {
const queryRoute = makeOperationRoute(relativeQueryRoute)
async function query(queryKey, queryArgs) {
const serverResult = await callOperation(queryRoute, queryArgs)
return getActiveOptimisticUpdates(queryKey).reduce(
@ -13,7 +15,8 @@ export function createQuery(queryRoute, entitiesUsed) {
)
}
query.queryCacheKey = [queryRoute]
query.queryCacheKey = [relativeQueryRoute]
query.route = queryRoute
addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed)
return query

View File

@ -1,6 +1,12 @@
import { UseQueryResult } from "@tanstack/react-query";
export type Query<Input, Output> = (args: Input) => Promise<Output>
import { type HttpMethod } from "../types";
export type Query<Input, Output> = {
(args: Input): Promise<Output>
queryCacheKey: string[]
route: { method: HttpMethod, path: string }
}
export function useQuery<Input, Output, Error = unknown>(
queryFn: Query<Input, Output>,

View File

@ -0,0 +1 @@
export { renderInContext, mockServer } from './vitest/helpers'

View File

@ -0,0 +1,67 @@
import { ReactElement } from 'react'
import { rest, type ResponseResolver, type RestContext } from 'msw'
import { setupServer, type SetupServer } from 'msw/node'
import { BrowserRouter as Router } from 'react-router-dom'
import { render, RenderResult, cleanup } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { beforeAll, afterEach, afterAll } from 'vitest'
import { Query } from '../../queries'
import config from '../../config'
import { HttpMethod } from '../../types'
// Inspired by the Tanstack React Query helper:
// https://github.com/TanStack/query/blob/4ae99561ca3383d6de3f4aad656a49ba4a17b57a/packages/react-query/src/__tests__/utils.tsx#L7-L26
export function renderInContext(ui: ReactElement): RenderResult {
const client = new QueryClient()
const { rerender, ...result } = render(
<QueryClientProvider client={client}><Router>{ui}</Router></QueryClientProvider>
)
return {
...result,
rerender: (rerenderUi: ReactElement) =>
rerender(
<QueryClientProvider client={client}><Router>{rerenderUi}</Router></QueryClientProvider>
)
}
}
type QueryRoute = Query<any, any>['route']
export function mockServer(): {
server: SetupServer,
mockQuery: ({ route }: { route: QueryRoute }, resJson: any) => void
} {
const server: SetupServer = setupServer()
beforeAll(() => server.listen())
afterEach(() => {
server.resetHandlers()
cleanup()
})
afterAll(() => server.close())
function mockQuery({ route }: { route: QueryRoute }, resJson: any): void {
if (!Object.values(HttpMethod).includes(route.method)) {
throw new Error(`Unsupported query method for mocking: ${route.method}. Supported method strings are: ${Object.values(HttpMethod).join(', ')}.`)
}
const url = `${config.apiUrl}${route.path}`
const responseHandler: ResponseResolver<any, RestContext, any> = (_req, res, ctx) => {
return res(ctx.json(resJson))
}
// NOTE: Technically, we only need to care about POST for Queries
// and GET for the /auth/me route. However, an additional use case
// for this function could be to mock APIs, so more methods are supported.
const handlers: Record<HttpMethod, Parameters<typeof server.use>[0]> = {
[HttpMethod.Get]: rest.get(url, responseHandler),
[HttpMethod.Post]: rest.post(url, responseHandler),
[HttpMethod.Put]: rest.put(url, responseHandler),
[HttpMethod.Delete]: rest.delete(url, responseHandler),
}
server.use(handlers[route.method])
}
return { server, mockQuery }
}

View File

@ -0,0 +1,4 @@
import matchers from '@testing-library/jest-dom/matchers'
import { expect } from 'vitest'
expect.extend(matchers)

View File

@ -0,0 +1,7 @@
// NOTE: This is enough to cover Operations and our APIs (src/Wasp/AppSpec/Api.hs).
export enum HttpMethod {
Get = 'GET',
Post = 'POST',
Put = 'PUT',
Delete = 'DELETE',
}

View File

@ -1,3 +1,4 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
@ -13,4 +14,8 @@ export default defineConfig({
build: {
outDir: 'build',
},
test: {
environment: 'jsdom',
setupFiles: ['./src/test/vitest/setup.ts'],
},
})

View File

@ -60,7 +60,7 @@ waspMigrate/.wasp/out/web-app/src/ext-src/vite-env.d.ts
waspMigrate/.wasp/out/web-app/src/ext-src/waspLogo.png
waspMigrate/.wasp/out/web-app/src/index.tsx
waspMigrate/.wasp/out/web-app/src/logo.png
waspMigrate/.wasp/out/web-app/src/operations/index.js
waspMigrate/.wasp/out/web-app/src/operations/index.ts
waspMigrate/.wasp/out/web-app/src/operations/resources.js
waspMigrate/.wasp/out/web-app/src/operations/updateHandlersMap.js
waspMigrate/.wasp/out/web-app/src/queries/core.d.ts
@ -70,6 +70,10 @@ waspMigrate/.wasp/out/web-app/src/queries/index.js
waspMigrate/.wasp/out/web-app/src/queryClient.js
waspMigrate/.wasp/out/web-app/src/router.jsx
waspMigrate/.wasp/out/web-app/src/storage.ts
waspMigrate/.wasp/out/web-app/src/test/index.ts
waspMigrate/.wasp/out/web-app/src/test/vitest/helpers.tsx
waspMigrate/.wasp/out/web-app/src/test/vitest/setup.ts
waspMigrate/.wasp/out/web-app/src/types.ts
waspMigrate/.wasp/out/web-app/src/universal/url.ts
waspMigrate/.wasp/out/web-app/src/utils.js
waspMigrate/.wasp/out/web-app/src/vite-env.d.ts

View File

@ -284,7 +284,7 @@
"file",
"web-app/package.json"
],
"54b7e66b5dc8c14c01fd424b118883370b24c4dba232120bfc808714d54baa11"
"7b7e86fe3ea2422742072141e2c56f80cdf82bfd140c32cf5431fa61d3b003cc"
],
[
[
@ -326,7 +326,7 @@
"file",
"web-app/src/actions/core.js"
],
"5c4dcdec74fb014a8edbb3d240bcbbfc829e201bce64132598b444db14a2bd45"
"f38003d51d9754952bf595a25fdb44580d88af3bcd6658848cf8a339a8240689"
],
[
[
@ -401,9 +401,9 @@
[
[
"file",
"web-app/src/operations/index.js"
"web-app/src/operations/index.ts"
],
"0d41d5791489c43814dfcfae09cf98a44418de9c36393cece15a2ff22da7d0b2"
"b48c8c12e212509e33482c3408b21caa180ca96df243925fb1aaa80bdfd6a734"
],
[
[
@ -424,21 +424,21 @@
"file",
"web-app/src/queries/core.d.ts"
],
"f0b289140e92738451be386ca73a2fd1c84e9951eb2f1b9c6c09dfa3079d0c74"
"a5bcc60f2a082359e537e1a1f231bd66abcdf4d7066531e68fd2aede3c04c059"
],
[
[
"file",
"web-app/src/queries/core.js"
],
"2daf5b414722204281d65e954ce862a6fc586e8907b202800694909d23957c5e"
"a0ba3524c8efff27abce03406ba661ac56197ddecbe665cd53a41409c6e4fc5d"
],
[
[
"file",
"web-app/src/queries/index.d.ts"
],
"efc70de9916a60e19e0c86aaf955b0be0c999ba5c30139c3b6b98bcc4d382091"
"e6b8b66d31eee3284c20085a4def388e4a4f34eb945e7cf8f2cccd2b6c3cbc2d"
],
[
[
@ -468,6 +468,34 @@
],
"1e35eb73e486c8f926337a8c8ddfc392639de3718bf28fdc3073b0ca97c864f7"
],
[
[
"file",
"web-app/src/test/index.ts"
],
"cb2e2dc33df8afc0d4453f4322a4e2af92f3345e9622e0416fa87e34d6acb9d8"
],
[
[
"file",
"web-app/src/test/vitest/helpers.tsx"
],
"f79b242670c09642684014d4eb0c7106a1b42cc3efce73f5b2f9b551854ce513"
],
[
[
"file",
"web-app/src/test/vitest/setup.ts"
],
"1c08b10e428cec3939e0ab269c9a02694e196de7c5dd9f18372424bdccbc5028"
],
[
[
"file",
"web-app/src/types.ts"
],
"3be2f95a2d7d2fa790e6a077edc241bd26db5e167ad3a4033a88bbb0262bb1c6"
],
[
[
"file",
@ -508,6 +536,6 @@
"file",
"web-app/vite.config.ts"
],
"0f62618ba1d03ad7fca703b5fadc64e1a4be7c553f121dbefe8e1f7d6efe2a19"
"0ab8b3892a5d5d25b85646ef30e8b2487904415021912e68670fab316b2ecf2d"
]
]

View File

@ -1 +1 @@
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.5.0"}],"devDependencies":[{"name":"vite","version":"^4.1.0"},{"name":"typescript","version":"^4.9.3"},{"name":"@types/react","version":"^17.0.53"},{"name":"@types/react-dom","version":"^17.0.19"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^1.0.1"}]}}
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.5.0"}],"devDependencies":[{"name":"vite","version":"^4.1.0"},{"name":"typescript","version":"^4.9.3"},{"name":"@types/react","version":"^17.0.53"},{"name":"@types/react-dom","version":"^17.0.19"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^1.0.1"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^12.1.5"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}

View File

@ -20,14 +20,20 @@
"react-router-dom": "^5.3.3"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5",
"@tsconfig/vite-react": "^1.0.1",
"@types/react": "^17.0.53",
"@types/react-dom": "^17.0.19",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react-swc": "^3.0.0",
"@vitest/ui": "^0.29.3",
"dotenv": "^16.0.3",
"jsdom": "^21.1.1",
"msw": "^1.1.0",
"typescript": "^4.9.3",
"vite": "^4.1.0"
"vite": "^4.1.0",
"vitest": "^0.29.3"
},
"engineStrict": true,
"engines": {

View File

@ -1,10 +1,12 @@
import { callOperation } from '../operations'
import { callOperation, makeOperationRoute } from '../operations'
import {
registerActionInProgress,
registerActionDone,
} from '../operations/resources'
export function createAction(actionRoute, entitiesUsed) {
export function createAction(relativeActionRoute, entitiesUsed) {
const actionRoute = makeOperationRoute(relativeActionRoute)
async function internalAction(args, specificOptimisticUpdateDefinitions) {
registerActionInProgress(specificOptimisticUpdateDefinitions)
try {

View File

@ -1,11 +0,0 @@
import api, { handleApiError } from '../api'
export async function callOperation(operationRoute, args) {
try {
const response = await api.post(`/${operationRoute}`, args)
return response.data
} catch (error) {
handleApiError(error)
}
}

View File

@ -0,0 +1,17 @@
import api, { handleApiError } from '../api'
import { HttpMethod } from '../types'
export type OperationRoute = { method: HttpMethod, path: string }
export async function callOperation(operationRoute: OperationRoute & { method: HttpMethod.Post }, args: any) {
try {
const response = await api.post(operationRoute.path, args)
return response.data
} catch (error) {
handleApiError(error)
}
}
export function makeOperationRoute(relativeOperationRoute: string): OperationRoute {
return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` }
}

View File

@ -1,3 +1,3 @@
export type Query<Input, Output> = (args: Input) => Promise<Output>
import { type Query } from './index'
export function createQuery<Input, Output>(queryRoute: string, entitiesUsed: any[]): Query<Input, Output>

View File

@ -1,10 +1,12 @@
import { callOperation } from '../operations'
import { callOperation, makeOperationRoute } from '../operations'
import {
addResourcesUsedByQuery,
getActiveOptimisticUpdates,
} from '../operations/resources'
export function createQuery(queryRoute, entitiesUsed) {
export function createQuery(relativeQueryRoute, entitiesUsed) {
const queryRoute = makeOperationRoute(relativeQueryRoute)
async function query(queryKey, queryArgs) {
const serverResult = await callOperation(queryRoute, queryArgs)
return getActiveOptimisticUpdates(queryKey).reduce(
@ -13,7 +15,8 @@ export function createQuery(queryRoute, entitiesUsed) {
)
}
query.queryCacheKey = [queryRoute]
query.queryCacheKey = [relativeQueryRoute]
query.route = queryRoute
addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed)
return query

View File

@ -1,6 +1,12 @@
import { UseQueryResult } from "@tanstack/react-query";
export type Query<Input, Output> = (args: Input) => Promise<Output>
import { type HttpMethod } from "../types";
export type Query<Input, Output> = {
(args: Input): Promise<Output>
queryCacheKey: string[]
route: { method: HttpMethod, path: string }
}
export function useQuery<Input, Output, Error = unknown>(
queryFn: Query<Input, Output>,

View File

@ -0,0 +1 @@
export { renderInContext, mockServer } from './vitest/helpers'

View File

@ -0,0 +1,67 @@
import { ReactElement } from 'react'
import { rest, type ResponseResolver, type RestContext } from 'msw'
import { setupServer, type SetupServer } from 'msw/node'
import { BrowserRouter as Router } from 'react-router-dom'
import { render, RenderResult, cleanup } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { beforeAll, afterEach, afterAll } from 'vitest'
import { Query } from '../../queries'
import config from '../../config'
import { HttpMethod } from '../../types'
// Inspired by the Tanstack React Query helper:
// https://github.com/TanStack/query/blob/4ae99561ca3383d6de3f4aad656a49ba4a17b57a/packages/react-query/src/__tests__/utils.tsx#L7-L26
export function renderInContext(ui: ReactElement): RenderResult {
const client = new QueryClient()
const { rerender, ...result } = render(
<QueryClientProvider client={client}><Router>{ui}</Router></QueryClientProvider>
)
return {
...result,
rerender: (rerenderUi: ReactElement) =>
rerender(
<QueryClientProvider client={client}><Router>{rerenderUi}</Router></QueryClientProvider>
)
}
}
type QueryRoute = Query<any, any>['route']
export function mockServer(): {
server: SetupServer,
mockQuery: ({ route }: { route: QueryRoute }, resJson: any) => void
} {
const server: SetupServer = setupServer()
beforeAll(() => server.listen())
afterEach(() => {
server.resetHandlers()
cleanup()
})
afterAll(() => server.close())
function mockQuery({ route }: { route: QueryRoute }, resJson: any): void {
if (!Object.values(HttpMethod).includes(route.method)) {
throw new Error(`Unsupported query method for mocking: ${route.method}. Supported method strings are: ${Object.values(HttpMethod).join(', ')}.`)
}
const url = `${config.apiUrl}${route.path}`
const responseHandler: ResponseResolver<any, RestContext, any> = (_req, res, ctx) => {
return res(ctx.json(resJson))
}
// NOTE: Technically, we only need to care about POST for Queries
// and GET for the /auth/me route. However, an additional use case
// for this function could be to mock APIs, so more methods are supported.
const handlers: Record<HttpMethod, Parameters<typeof server.use>[0]> = {
[HttpMethod.Get]: rest.get(url, responseHandler),
[HttpMethod.Post]: rest.post(url, responseHandler),
[HttpMethod.Put]: rest.put(url, responseHandler),
[HttpMethod.Delete]: rest.delete(url, responseHandler),
}
server.use(handlers[route.method])
}
return { server, mockQuery }
}

View File

@ -0,0 +1,4 @@
import matchers from '@testing-library/jest-dom/matchers'
import { expect } from 'vitest'
expect.extend(matchers)

View File

@ -0,0 +1,7 @@
// NOTE: This is enough to cover Operations and our APIs (src/Wasp/AppSpec/Api.hs).
export enum HttpMethod {
Get = 'GET',
Post = 'POST',
Put = 'PUT',
Delete = 'DELETE',
}

View File

@ -1,3 +1,4 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
@ -13,4 +14,8 @@ export default defineConfig({
build: {
outDir: 'build',
},
test: {
environment: 'jsdom',
setupFiles: ['./src/test/vitest/setup.ts'],
},
})

View File

@ -0,0 +1,46 @@
import { test, expect } from 'vitest'
import { screen } from '@testing-library/react'
import { mockServer, renderInContext } from '@wasp/test'
import getTasks from '@wasp/queries/getTasks'
import Todo, { areThereAnyTasks } from './Todo'
import { App } from './App'
import { getMe } from '@wasp/auth/useAuth'
test('areThereAnyTasks', () => {
expect(areThereAnyTasks([])).toBe(false)
})
const { mockQuery } = mockServer()
const mockTasks = [{
id: 1,
description: 'test todo 1',
isDone: true,
userId: 1
}]
test('handles mock data', async () => {
mockQuery(getTasks, mockTasks)
renderInContext(<Todo />)
await screen.findByText('test todo 1')
expect(screen.getByRole('checkbox')).toBeChecked()
screen.debug()
})
test('handles multiple mock data sources', async () => {
mockQuery(getMe, { username: 'elon' })
mockQuery(getTasks, mockTasks)
renderInContext(<App><Todo /></App>)
await screen.findByText('elon')
expect(screen.getByRole('checkbox')).toBeChecked()
screen.debug()
})

View File

@ -14,7 +14,7 @@ type GetTasksError = { message: string }
type NonEmptyArray<T> = [T, ...T[]]
function areThereAnyTasks(
export function areThereAnyTasks(
tasks: Task[] | undefined
): tasks is NonEmptyArray<Task> {
return !!(tasks && tasks.length > 0)

View File

@ -4,21 +4,19 @@ import { Link } from 'react-router-dom'
import SignupForm from '@wasp/auth/forms/Signup'
import getNumTasks from '@wasp/queries/getNumTasks'
import { useQuery } from '@wasp/queries'
import { getTotalTaskCountMessage } from './helpers'
const Signup = () => {
const { data: numTasks } = useQuery(getNumTasks)
return (
<>
<SignupForm/>
<br/>
<SignupForm />
<br />
<span>
I already have an account (<Link to="/login">go to login</Link>).
</span>
<br/>
<span>
Number of tasks already created: {numTasks}
</span>
<br />
<span>{getTotalTaskCountMessage(numTasks)}</span>
</>
)
}

View File

@ -0,0 +1,20 @@
import { describe, it, expect } from 'vitest'
import { getTotalTaskCountMessage } from './helpers'
describe('helpers', () => {
it('not loaded yet -> empty string', () => {
expect(getTotalTaskCountMessage()).toBe('')
})
it('no tasks -> 0 tasks message', () => {
expect(getTotalTaskCountMessage(0)).toBe('No tasks created, yet.')
})
it('one task -> 1 task message', () => {
expect(getTotalTaskCountMessage(1)).toBe('There is just one task.')
})
it('multiple tasks -> default message', () => {
expect(getTotalTaskCountMessage(2)).toBe(
'There are 2 tasks created so far.'
)
})
})

View File

@ -0,0 +1,12 @@
export function getTotalTaskCountMessage(numTasks?: number): string {
if (numTasks === undefined) {
return ''
}
if (numTasks === 0) {
return 'No tasks created, yet.'
}
if (numTasks === 1) {
return 'There is just one task.'
}
return `There are ${numTasks} tasks created so far.`
}

View File

@ -1,6 +1,7 @@
module Wasp.Generator
( writeWebAppCode,
Wasp.Generator.Start.start,
Wasp.Generator.Test.testWebApp,
ProjectRootDir,
)
where
@ -23,6 +24,7 @@ import Wasp.Generator.Monad (Generator, GeneratorError, GeneratorWarning, runGen
import Wasp.Generator.ServerGenerator (genServer)
import Wasp.Generator.Setup (runSetup)
import qualified Wasp.Generator.Start
import qualified Wasp.Generator.Test
import Wasp.Generator.WebAppGenerator (genWebApp)
import Wasp.Generator.WriteFileDrafts (synchronizeFileDraftsWithDisk)
import Wasp.Message (SendMessage)

View File

@ -0,0 +1,22 @@
module Wasp.Generator.Test
( testWebApp,
)
where
import Control.Concurrent (newChan)
import Control.Concurrent.Async (concurrently)
import StrongPath (Abs, Dir, Path')
import System.Exit (ExitCode (..))
import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.Job.IO (readJobMessagesAndPrintThemPrefixed)
import qualified Wasp.Generator.WebAppGenerator.Test as WebAppTest
testWebApp :: [String] -> Path' Abs (Dir ProjectRootDir) -> IO (Either String ())
testWebApp args projectDir = do
chan <- newChan
let testWebAppJob = WebAppTest.testWebApp args projectDir chan
(testExitCode, _) <-
testWebAppJob `concurrently` readJobMessagesAndPrintThemPrefixed chan
case testExitCode of
ExitSuccess -> return $ Right ()
ExitFailure code -> return $ Left $ "Tests failed with exit code " ++ show code ++ "."

View File

@ -59,6 +59,9 @@ genWebApp spec = do
genFileCopy [relfile|tsconfig.json|],
genFileCopy [relfile|tsconfig.node.json|],
genFileCopy [relfile|vite.config.ts|],
genFileCopy [relfile|src/test/vitest/setup.ts|],
genFileCopy [relfile|src/test/vitest/helpers.tsx|],
genFileCopy [relfile|src/test/index.ts|],
genFileCopy [relfile|netlify.toml|],
genPackageJson spec (npmDepsForWasp spec),
genNpmrc,
@ -144,6 +147,7 @@ npmDepsForWasp spec =
-- when updating Vite or React versions
("@tsconfig/vite-react", "^1.0.1")
]
++ depsRequiredForTesting
}
depsRequiredByTailwind :: AppSpec -> [AS.Dependency.Dependency]
@ -157,6 +161,17 @@ depsRequiredByTailwind spec =
]
else []
depsRequiredForTesting :: [AS.Dependency.Dependency]
depsRequiredForTesting =
AS.Dependency.fromList
[ ("vitest", "^0.29.3"),
("@vitest/ui", "^0.29.3"),
("jsdom", "^21.1.1"),
("@testing-library/react", "^12.1.5"),
("@testing-library/jest-dom", "^5.16.5"),
("msw", "^1.1.0")
]
genGitignore :: Generator FileDraft
genGitignore =
return $
@ -217,6 +232,7 @@ genSrcDir spec =
copyTmplFile [relfile|config.js|],
copyTmplFile [relfile|queryClient.js|],
copyTmplFile [relfile|utils.js|],
copyTmplFile [relfile|types.ts|],
copyTmplFile [relfile|vite-env.d.ts|],
-- Generates api.js file which contains token management and configured api (e.g. axios) instance.
copyTmplFile [relfile|api.ts|],

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