safe-action-testing

安装量: 39
排名: #18415

安装

npx skills add https://github.com/next-safe-action/skills --skill safe-action-testing

Testing next-safe-action Testing Actions Directly Server actions are async functions — call them directly in tests: // src/tests/actions.test.ts import { describe , it , expect , vi } from "vitest" ; import { createUser } from "@/app/actions" ; describe ( "createUser" , ( ) => { it ( "returns user data on valid input" , async ( ) => { const result = await createUser ( { name : "Alice" , email : "alice@example.com" } ) ; expect ( result . data ) . toEqual ( { id : expect . any ( String ) , name : "Alice" , } ) ; expect ( result . serverError ) . toBeUndefined ( ) ; expect ( result . validationErrors ) . toBeUndefined ( ) ; } ) ; it ( "returns validation errors on invalid input" , async ( ) => { const result = await createUser ( { name : "" , email : "not-an-email" } ) ; expect ( result . data ) . toBeUndefined ( ) ; expect ( result . validationErrors ) . toBeDefined ( ) ; expect ( result . validationErrors ?. email ?. _errors ) . toContain ( "Invalid email" ) ; } ) ; it ( "returns server error on duplicate email" , async ( ) => { // Setup: create first user await createUser ( { name : "Alice" , email : "alice@example.com" } ) ; // Attempt duplicate const result = await createUser ( { name : "Bob" , email : "alice@example.com" } ) ; // If using returnValidationErrors: expect ( result . validationErrors ?. email ?. _errors ) . toContain ( "Email already in use" ) ; // OR if using throw + handleServerError: // expect(result.serverError).toBe("Email already in use"); } ) ; } ) ; Testing Actions with Bind Args import { updatePost } from "@/app/actions" ; describe ( "updatePost" , ( ) => { it ( "updates the post" , async ( ) => { const postId = "123e4567-e89b-12d3-a456-426614174000" ; const boundAction = updatePost . bind ( null , postId ) ; const result = await boundAction ( { title : "Updated Title" , content : "Updated content" , } ) ; expect ( result . data ) . toEqual ( { success : true } ) ; } ) ; it ( "returns validation error for invalid postId" , async ( ) => { const boundAction = updatePost . bind ( null , "not-a-uuid" ) ; // Bind args validation errors throw ActionBindArgsValidationError await expect ( boundAction ( { title : "Test" , content : "Test" } ) ) . rejects . toThrow ( ) ; } ) ; } ) ; Testing Middleware Test middleware behavior by creating actions with specific middleware chains: import { describe , it , expect , vi } from "vitest" ; import { createSafeActionClient } from "next-safe-action" ; import { z } from "zod" ; // Mock auth vi . mock ( "@/lib/auth" , ( ) => ( { getSession : vi . fn ( ) , } ) ) ; import { getSession } from "@/lib/auth" ; const authClient = createSafeActionClient ( ) . use ( async ( { next } ) => { const session = await getSession ( ) ; if ( ! session ?. user ) throw new Error ( "Unauthorized" ) ; return next ( { ctx : { userId : session . user . id } } ) ; } ) ; const testAction = authClient . action ( async ( { ctx } ) => { return { userId : ctx . userId } ; } ) ; describe ( "auth middleware" , ( ) => { it ( "passes userId to action when authenticated" , async ( ) => { vi . mocked ( getSession ) . mockResolvedValue ( { user : { id : "user-1" , role : "user" } , } ) ; const result = await testAction ( ) ; expect ( result . data ) . toEqual ( { userId : "user-1" } ) ; } ) ; it ( "returns server error when unauthenticated" , async ( ) => { vi . mocked ( getSession ) . mockResolvedValue ( null ) ; const result = await testAction ( ) ; expect ( result . serverError ) . toBeDefined ( ) ; } ) ; } ) ; Testing Hooks Use React Testing Library's renderHook : import { describe , it , expect , vi } from "vitest" ; import { renderHook , act , waitFor } from "@testing-library/react" ; import { useAction } from "next-safe-action/hooks" ; // Mock the action const mockAction = vi . fn ( ) ; describe ( "useAction" , ( ) => { it ( "starts idle" , ( ) => { const { result } = renderHook ( ( ) => useAction ( mockAction ) ) ; expect ( result . current . isIdle ) . toBe ( true ) ; expect ( result . current . isExecuting ) . toBe ( false ) ; expect ( result . current . result ) . toEqual ( { } ) ; } ) ; it ( "executes and returns data" , async ( ) => { mockAction . mockResolvedValue ( { data : { id : "1" } } ) ; const { result } = renderHook ( ( ) => useAction ( mockAction , { onSuccess : vi . fn ( ) , } ) ) ; act ( ( ) => { result . current . execute ( { name : "Alice" } ) ; } ) ; await waitFor ( ( ) => { expect ( result . current . hasSucceeded ) . toBe ( true ) ; } ) ; expect ( result . current . result . data ) . toEqual ( { id : "1" } ) ; } ) ; it ( "handles server errors" , async ( ) => { mockAction . mockResolvedValue ( { serverError : "Something went wrong" } ) ; const onError = vi . fn ( ) ; const { result } = renderHook ( ( ) => useAction ( mockAction , { onError } ) ) ; act ( ( ) => { result . current . execute ( { } ) ; } ) ; await waitFor ( ( ) => { expect ( result . current . hasErrored ) . toBe ( true ) ; } ) ; expect ( result . current . result . serverError ) . toBe ( "Something went wrong" ) ; expect ( onError ) . toHaveBeenCalled ( ) ; } ) ; it ( "resets state" , async ( ) => { mockAction . mockResolvedValue ( { data : { id : "1" } } ) ; const { result } = renderHook ( ( ) => useAction ( mockAction ) ) ; act ( ( ) => { result . current . execute ( { } ) ; } ) ; await waitFor ( ( ) => { expect ( result . current . hasSucceeded ) . toBe ( true ) ; } ) ; act ( ( ) => { result . current . reset ( ) ; } ) ; expect ( result . current . isIdle ) . toBe ( true ) ; expect ( result . current . result ) . toEqual ( { } ) ; } ) ; } ) ; Testing Validation Errors import { flattenValidationErrors , formatValidationErrors } from "next-safe-action" ; describe ( "validation error utilities" , ( ) => { const formatted = { _errors : [ "Form error" ] , email : { _errors : [ "Invalid email" ] } , name : { _errors : [ "Too short" , "Must start with uppercase" ] } , } ; it ( "flattenValidationErrors" , ( ) => { const flattened = flattenValidationErrors ( formatted ) ; expect ( flattened . formErrors ) . toEqual ( [ "Form error" ] ) ; expect ( flattened . fieldErrors . email ) . toEqual ( [ "Invalid email" ] ) ; expect ( flattened . fieldErrors . name ) . toEqual ( [ "Too short" , "Must start with uppercase" ] ) ; } ) ; it ( "formatValidationErrors is identity" , ( ) => { expect ( formatValidationErrors ( formatted ) ) . toBe ( formatted ) ; } ) ; } ) ; Mocking Framework Errors import { vi } from "vitest" ; // Mock Next.js navigation vi . mock ( "next/navigation" , ( ) => ( { // Digest formats are Next.js internals — may change across versions redirect : vi . fn ( ( url : string ) => { throw Object . assign ( new Error ( "NEXT_REDIRECT" ) , { digest : NEXT_REDIRECT;push; ${ url } ;303; , } ) ; } ) , notFound : vi . fn ( ( ) => { throw Object . assign ( new Error ( "NEXT_NOT_FOUND" ) , { digest : "NEXT_HTTP_ERROR_FALLBACK;404" , } ) ; } ) , } ) ) ; Test File Organization Follow the project convention: packages/next-safe-action/src/tests/ ├── happy-path.test.ts # Core happy path tests ├── validation-errors.test.ts # Validation error utilities ├── middleware.test.ts # Middleware chain behavior ├── navigation-errors.test.ts # Framework error handling ├── navigation-immediate-throw.test.ts # Immediate navigation throws ├── server-error.test.ts # Server error handling ├── bind-args-validation-errors.test.ts # Bind args validation ├── returnvalidationerrors.test.ts # returnValidationErrors behavior ├── input-schema.test.ts # Input schema tests ├── metadata.test.ts # Metadata tests ├── action-callbacks.test.ts # Server-level callbacks └── hooks-utils.test.ts # Hook utilities Run tests:

All tests

pnpm run test:lib

Single file

cd packages/next-safe-action && npx vitest run ./src/tests/action-builder.test.ts

返回排行榜