@@ -589,7 +589,7 @@ export default scenario("User CRUD API", { tags: ["api", "integration"] })
589589 })
590590 .step (" Get user" , async (ctx ) => {
591591 const { http } = ctx .resources ;
592- const { id } = ctx .previous ! ;
592+ const { id } = ctx .previous ;
593593 const res = await http .get (` /users/${id } ` );
594594 expect (res ).toBeOk ().toHaveStatus (200 ).toHaveDataMatching ({
595595 id ,
@@ -599,7 +599,7 @@ export default scenario("User CRUD API", { tags: ["api", "integration"] })
599599 })
600600 .step (" Update user" , async (ctx ) => {
601601 const { http } = ctx .resources ;
602- const { id } = ctx .previous ! ;
602+ const { id } = ctx .previous ;
603603 const res = await http .patch (` /users/${id } ` , { name: " Bob" });
604604 expect (res ).toBeOk ().toHaveStatus (200 ).toHaveDataMatching ({
605605 name: " Bob" ,
@@ -608,7 +608,7 @@ export default scenario("User CRUD API", { tags: ["api", "integration"] })
608608 })
609609 .step (" Delete user" , async (ctx ) => {
610610 const { http } = ctx .resources ;
611- const { id } = ctx .previous ! ;
611+ const { id } = ctx .previous ;
612612 const res = await http .delete (` /users/${id } ` );
613613 expect (res ).toBeOk ().toHaveStatus (204 );
614614 })
@@ -653,13 +653,13 @@ export default scenario("Database Transaction", { tags: ["db", "postgres"] })
653653 " INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id" ,
654654 [" Alice" , " alice@example.com" ],
655655 );
656- return insert .rows .first ();
656+ return insert .rows .first ()! ;
657657 });
658658 return result ;
659659 })
660660 .step (" Verify user exists" , async (ctx ) => {
661661 const { pg } = ctx .resources ;
662- const { id } = ctx .previous ! ;
662+ const { id } = ctx .previous ;
663663 const result = await pg .query <{ name: string }>(
664664 " SELECT name FROM users WHERE id = $1" ,
665665 [id ],
@@ -757,7 +757,7 @@ export default scenario("Full Stack Test", {
757757 })
758758 .step (" Verify in database" , async (ctx ) => {
759759 const { pg } = ctx .resources ;
760- const { id } = ctx .previous ! ;
760+ const { id } = ctx .previous ;
761761 const result = await pg .query (
762762 " SELECT * FROM items WHERE id = $1" ,
763763 [id ],
@@ -796,33 +796,52 @@ scenario("Email Test")
796796Return data that subsequent steps need. This enables type-safe data flow through
797797` ctx.previous ` .
798798
799- ``` typescript
800- import { client , scenario } from " jsr:@probitas/probitas" ;
799+ Good - returns data needed by next step:
801800
802- const data = { name: " Alice" , email: " alice@example.com" };
801+ ``` typescript
802+ import { client , expect , scenario } from " jsr:@probitas/probitas" ;
803803
804- // Good - returns data needed by next step
805- scenario (" Good Example" )
804+ scenario (" User creation and retrieval" )
806805 .resource (
807806 " http" ,
808807 () => client .http .createHttpClient ({ url: " http://localhost:8080" }),
809808 )
810809 .step (" Create user" , async (ctx ) => {
811- const { http } = ctx .resources ;
812- const res = await http .post (" /users" , data );
813- return res .data <{ id: number }>();
810+ const res = await ctx .resources .http .post (" /users" , {
811+ name: " Alice" ,
812+ email: " alice@example.com" ,
813+ });
814+ return res .data <{ id: number }>()! ;
815+ })
816+ .step (" Get created user" , async (ctx ) => {
817+ // ctx.previous is typed as { id: number }
818+ const res = await ctx .resources .http .get (` /users/${ctx .previous .id } ` );
819+ expect (res ).toHaveStatus (200 ).toHaveDataMatching ({ name: " Alice" });
814820 })
815821 .build ();
822+ ```
816823
817- // Avoid - loses useful data
818- scenario (" Avoid Example" )
824+ Avoid - loses useful data:
825+
826+ ``` ts
827+ import { client , expect , scenario } from " jsr:@probitas/probitas" ;
828+
829+ scenario (" User creation and retrieval" )
819830 .resource (
820831 " http" ,
821832 () => client .http .createHttpClient ({ url: " http://localhost:8080" }),
822833 )
823834 .step (" Create user" , async (ctx ) => {
824- const { http } = ctx .resources ;
825- await http .post (" /users" , data );
835+ await ctx .resources .http .post (" /users" , {
836+ name: " Alice" ,
837+ email: " alice@example.com" ,
838+ });
839+ // No return value - next step can't access created user's ID!
840+ })
841+ .step (" Get created user" , async (ctx ) => {
842+ // ctx.previous is undefined - we lost the user ID!
843+ const res = await ctx .resources .http .get (" /users/???" );
844+ expect (res ).toHaveStatus (200 );
826845 })
827846 .build ();
828847```
@@ -906,3 +925,196 @@ scenario("Avoid Setup Example")
906925 })
907926 .build ();
908927```
928+
929+ ### Split Scenarios for Better Organization
930+
931+ Export multiple focused scenarios from a single file rather than one large
932+ scenario. This provides several benefits:
933+
934+ - ** Parallel execution** : Separate scenarios can run concurrently, improving
935+ overall test speed
936+ - ** Tag filtering** : Each scenario can have its own tags for fine-grained test
937+ selection
938+ - ** Clear failure identification** : When a test fails, you immediately know
939+ which specific scenario failed
940+
941+ Good - multiple focused scenarios:
942+
943+ ``` typescript
944+ // user-validation.probitas.ts
945+ import { client , expect , scenario } from " jsr:@probitas/probitas" ;
946+
947+ export default [
948+ scenario (" User validation - rejects empty name" , {
949+ tags: [" api" , " validation" ],
950+ })
951+ .resource (
952+ " http" ,
953+ () => client .http .createHttpClient ({ url: " http://localhost:8080" }),
954+ )
955+ .step (" Create user with empty name" , async (ctx ) => {
956+ const res = await ctx .resources .http .post (" /users" , { name: " " });
957+ expect (res ).toHaveStatus (400 );
958+ })
959+ .build (),
960+
961+ scenario (" User validation - rejects duplicate email" , {
962+ tags: [" api" , " validation" ],
963+ })
964+ .resource (
965+ " http" ,
966+ () => client .http .createHttpClient ({ url: " http://localhost:8080" }),
967+ )
968+ .setup (async (ctx ) => {
969+ const res = await ctx .resources .http .post (" /users" , {
970+ name: " Alice" ,
971+ email: " alice@example.com" ,
972+ });
973+ const user = res .data <{ id: number }>();
974+ return async () => {
975+ await ctx .resources .http .delete (` /users/${user ! .id } ` );
976+ };
977+ })
978+ .step (" Create user with duplicate email" , async (ctx ) => {
979+ const res = await ctx .resources .http .post (" /users" , {
980+ name: " Bob" ,
981+ email: " alice@example.com" ,
982+ });
983+ expect (res ).toHaveStatus (409 );
984+ })
985+ .build (),
986+
987+ scenario (" User validation - rejects invalid email format" , {
988+ tags: [" api" , " validation" ],
989+ })
990+ .resource (
991+ " http" ,
992+ () => client .http .createHttpClient ({ url: " http://localhost:8080" }),
993+ )
994+ .step (" Create user with invalid email" , async (ctx ) => {
995+ const res = await ctx .resources .http .post (" /users" , {
996+ name: " Alice" ,
997+ email: " not-an-email" ,
998+ });
999+ expect (res ).toHaveStatus (400 );
1000+ })
1001+ .build (),
1002+ ];
1003+ ```
1004+
1005+ Avoid - one monolithic scenario:
1006+
1007+ ``` ts
1008+ // user-validation.probitas.ts
1009+ import { client , expect , scenario } from " jsr:@probitas/probitas" ;
1010+
1011+ export default scenario (" User validation" , { tags: [" api" , " validation" ] })
1012+ .resource (
1013+ " http" ,
1014+ () => client .http .createHttpClient ({ url: " http://localhost:8080" }),
1015+ )
1016+ .step (" Reject empty name" , async (ctx ) => {
1017+ const res = await ctx .resources .http .post (" /users" , { name: " " });
1018+ expect (res ).toHaveStatus (400 );
1019+ })
1020+ .step (" Reject duplicate email" , async (ctx ) => {
1021+ // Create first user
1022+ await ctx .resources .http .post (" /users" , {
1023+ name: " Alice" ,
1024+ email: " alice@example.com" ,
1025+ });
1026+ // Try to create another user with same email
1027+ const res = await ctx .resources .http .post (" /users" , {
1028+ name: " Bob" ,
1029+ email: " alice@example.com" ,
1030+ });
1031+ expect (res ).toHaveStatus (409 );
1032+ })
1033+ .step (" Reject invalid email format" , async (ctx ) => {
1034+ const res = await ctx .resources .http .post (" /users" , {
1035+ name: " Alice" ,
1036+ email: " not-an-email" ,
1037+ });
1038+ expect (res ).toHaveStatus (400 );
1039+ })
1040+ .build ();
1041+ ```
1042+
1043+ However, when steps are part of a sequential workflow where each step depends on
1044+ the previous one, keep them in a single scenario. Use ` ctx.previous ` to pass
1045+ data between steps:
1046+
1047+ Good - sequential CRUD operations as a single scenario:
1048+
1049+ ``` ts
1050+ // user-crud.probitas.ts
1051+ import { client , expect , scenario } from " jsr:@probitas/probitas" ;
1052+
1053+ export default scenario (" User CRUD workflow" , { tags: [" api" , " crud" ] })
1054+ .resource (
1055+ " http" ,
1056+ () => client .http .createHttpClient ({ url: " http://localhost:8080" }),
1057+ )
1058+ .step (" Create user" , async (ctx ) => {
1059+ const res = await ctx .resources .http .post (" /users" , {
1060+ name: " Alice" ,
1061+ email: " alice@example.com" ,
1062+ });
1063+ expect (res ).toHaveStatus (201 );
1064+ return res .data <{ id: number }>()! ;
1065+ })
1066+ .step (" Get user" , async (ctx ) => {
1067+ const res = await ctx .resources .http .get (` /users/${ctx .previous .id } ` );
1068+ expect (res ).toHaveStatus (200 ).toHaveDataMatching ({ name: " Alice" });
1069+ return ctx .previous ;
1070+ })
1071+ .step (" Update user" , async (ctx ) => {
1072+ const res = await ctx .resources .http .patch (` /users/${ctx .previous .id } ` , {
1073+ name: " Alice Smith" ,
1074+ });
1075+ expect (res ).toHaveStatus (200 );
1076+ return ctx .previous ;
1077+ })
1078+ .step (" Delete user" , async (ctx ) => {
1079+ const res = await ctx .resources .http .delete (` /users/${ctx .previous .id } ` );
1080+ expect (res ).toHaveStatus (204 );
1081+ })
1082+ .build ();
1083+ ```
1084+
1085+ Avoid - splitting sequential workflow into separate scenarios:
1086+
1087+ ``` ts
1088+ // user-crud.probitas.ts - DON'T do this!
1089+ import { client , expect , scenario } from " jsr:@probitas/probitas" ;
1090+
1091+ // These scenarios cannot run in parallel - they share state!
1092+ export default [
1093+ scenario (" User CRUD - create" , { tags: [" api" , " crud" ] })
1094+ .resource (
1095+ " http" ,
1096+ () => client .http .createHttpClient ({ url: " http://localhost:8080" }),
1097+ )
1098+ .step (" Create user" , async (ctx ) => {
1099+ const res = await ctx .resources .http .post (" /users" , {
1100+ name: " Alice" ,
1101+ email: " alice@example.com" ,
1102+ });
1103+ expect (res ).toHaveStatus (201 );
1104+ // Problem: How do we pass the user ID to the next scenario?
1105+ })
1106+ .build (),
1107+
1108+ scenario (" User CRUD - get" , { tags: [" api" , " crud" ] })
1109+ .resource (
1110+ " http" ,
1111+ () => client .http .createHttpClient ({ url: " http://localhost:8080" }),
1112+ )
1113+ .step (" Get user" , async (ctx ) => {
1114+ // Problem: We don't know the user ID from the previous scenario!
1115+ const res = await ctx .resources .http .get (" /users/???" );
1116+ expect (res ).toHaveStatus (200 );
1117+ })
1118+ .build (),
1119+ ];
1120+ ```
0 commit comments