mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-23 17:13:40 +03:00
Adds vitest
for testing client code (#1055)
This commit is contained in:
parent
6dbeedca60
commit
5d2b278e75
@ -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.
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
48
waspc/cli/src/Wasp/Cli/Command/Test.hs
Normal file
48
waspc/cli/src/Wasp/Cli/Command/Test.hs
Normal 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 ()
|
@ -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 {
|
||||
|
@ -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' }
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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}` }
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
|
@ -0,0 +1 @@
|
||||
export { renderInContext, mockServer } from './vitest/helpers'
|
@ -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 }
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import matchers from '@testing-library/jest-dom/matchers'
|
||||
import { expect } from 'vitest'
|
||||
|
||||
expect.extend(matchers)
|
7
waspc/data/Generator/templates/react-app/src/types.ts
Normal file
7
waspc/data/Generator/templates/react-app/src/types.ts
Normal 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',
|
||||
}
|
@ -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'],
|
||||
},
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
]
|
||||
]
|
@ -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"}]}}
|
@ -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": {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
17
waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/operations/index.ts
generated
Normal file
17
waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/operations/index.ts
generated
Normal 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}` }
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
|
1
waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/test/index.ts
generated
Normal file
1
waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/test/index.ts
generated
Normal file
@ -0,0 +1 @@
|
||||
export { renderInContext, mockServer } from './vitest/helpers'
|
@ -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 }
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import matchers from '@testing-library/jest-dom/matchers'
|
||||
import { expect } from 'vitest'
|
||||
|
||||
expect.extend(matchers)
|
7
waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/types.ts
generated
Normal file
7
waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/types.ts
generated
Normal 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',
|
||||
}
|
@ -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'],
|
||||
},
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
]
|
||||
]
|
@ -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"}]}}
|
@ -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": {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
17
waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/operations/index.ts
generated
Normal file
17
waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/operations/index.ts
generated
Normal 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}` }
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
|
1
waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/test/index.ts
generated
Normal file
1
waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/test/index.ts
generated
Normal file
@ -0,0 +1 @@
|
||||
export { renderInContext, mockServer } from './vitest/helpers'
|
@ -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 }
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import matchers from '@testing-library/jest-dom/matchers'
|
||||
import { expect } from 'vitest'
|
||||
|
||||
expect.extend(matchers)
|
7
waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/types.ts
generated
Normal file
7
waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/types.ts
generated
Normal 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',
|
||||
}
|
@ -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'],
|
||||
},
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
]
|
||||
]
|
@ -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"}]}}
|
@ -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": {
|
||||
|
@ -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 {
|
||||
|
@ -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' }
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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}` }
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
|
@ -0,0 +1 @@
|
||||
export { renderInContext, mockServer } from './vitest/helpers'
|
@ -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 }
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import matchers from '@testing-library/jest-dom/matchers'
|
||||
import { expect } from 'vitest'
|
||||
|
||||
expect.extend(matchers)
|
@ -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',
|
||||
}
|
@ -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'],
|
||||
},
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
]
|
||||
]
|
@ -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"}]}}
|
@ -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": {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
17
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/operations/index.ts
generated
Normal file
17
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/operations/index.ts
generated
Normal 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}` }
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
|
1
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/test/index.ts
generated
Normal file
1
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/test/index.ts
generated
Normal file
@ -0,0 +1 @@
|
||||
export { renderInContext, mockServer } from './vitest/helpers'
|
67
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/test/vitest/helpers.tsx
generated
Normal file
67
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/test/vitest/helpers.tsx
generated
Normal 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 }
|
||||
}
|
4
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/test/vitest/setup.ts
generated
Normal file
4
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/test/vitest/setup.ts
generated
Normal file
@ -0,0 +1,4 @@
|
||||
import matchers from '@testing-library/jest-dom/matchers'
|
||||
import { expect } from 'vitest'
|
||||
|
||||
expect.extend(matchers)
|
7
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/types.ts
generated
Normal file
7
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/types.ts
generated
Normal 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',
|
||||
}
|
@ -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'],
|
||||
},
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
]
|
||||
]
|
@ -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"}]}}
|
@ -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": {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
17
waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/operations/index.ts
generated
Normal file
17
waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/operations/index.ts
generated
Normal 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}` }
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
|
1
waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/test/index.ts
generated
Normal file
1
waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/test/index.ts
generated
Normal file
@ -0,0 +1 @@
|
||||
export { renderInContext, mockServer } from './vitest/helpers'
|
@ -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 }
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import matchers from '@testing-library/jest-dom/matchers'
|
||||
import { expect } from 'vitest'
|
||||
|
||||
expect.extend(matchers)
|
7
waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/types.ts
generated
Normal file
7
waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/types.ts
generated
Normal 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',
|
||||
}
|
@ -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'],
|
||||
},
|
||||
})
|
||||
|
46
waspc/examples/todoApp/src/client/Todo.test.tsx
Normal file
46
waspc/examples/todoApp/src/client/Todo.test.tsx
Normal 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()
|
||||
})
|
@ -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)
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
20
waspc/examples/todoApp/src/client/pages/auth/helpers.test.ts
Normal file
20
waspc/examples/todoApp/src/client/pages/auth/helpers.test.ts
Normal 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.'
|
||||
)
|
||||
})
|
||||
})
|
12
waspc/examples/todoApp/src/client/pages/auth/helpers.ts
Normal file
12
waspc/examples/todoApp/src/client/pages/auth/helpers.ts
Normal 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.`
|
||||
}
|
@ -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)
|
||||
|
22
waspc/src/Wasp/Generator/Test.hs
Normal file
22
waspc/src/Wasp/Generator/Test.hs
Normal 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 ++ "."
|
@ -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
Loading…
Reference in New Issue
Block a user