@@ -980,6 +980,158 @@ describe("graph diagnostics", () => {
980980 expect ( reachDiags [ 0 ] ?. nodeId ) . toBe ( "act_sign_up" )
981981 } )
982982 } )
983+
984+ describe ( "orphaned-flow diagnostic" , ( ) => {
985+ it ( "does not emit orphaned-flow when there are no flows" , ( ) => {
986+ const screenDef = screen ( "NoFlowsForOrphan" , $ => {
987+ const email = $ . state . text ( "email" )
988+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
989+ const login = $ . act ( "Log in" )
990+ $ . surface ( "main" ) . contains ( emailAsk , login )
991+ } )
992+ const inspected = inspectScreen ( screenDef )
993+ const diags = inspected . diagnostics . filter ( d => d . code === "orphaned-flow" )
994+ expect ( diags ) . toHaveLength ( 0 )
995+ } )
996+
997+ it ( "does not emit orphaned-flow for empty flow" , ( ) => {
998+ const screenDef = screen ( "EmptyFlow" , $ => {
999+ const email = $ . state . text ( "email" )
1000+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
1001+ const login = $ . act ( "Log in" )
1002+ $ . surface ( "main" ) . contains ( emailAsk , login )
1003+ $ . flow ( "empty" )
1004+ } )
1005+ const inspected = inspectScreen ( screenDef )
1006+ const diags = inspected . diagnostics . filter ( d => d . code === "orphaned-flow" )
1007+ expect ( diags ) . toHaveLength ( 0 )
1008+ } )
1009+
1010+ it ( "emits orphaned-flow when all flow steps are unsurfaced" , ( ) => {
1011+ const screenDef = screen ( "AllStepsUnsurfaced" , $ => {
1012+ const email = $ . state . text ( "email" )
1013+ const pwd = $ . state . text ( "password" )
1014+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
1015+ const pwdAsk = $ . ask ( "Password" , pwd ) . private ( )
1016+ const login = $ . act ( "Log in" )
1017+ $ . surface ( "main" ) . contains ( login )
1018+ $ . flow ( "login" ) . startsWith ( emailAsk ) . then ( pwdAsk )
1019+ } )
1020+ const inspected = inspectScreen ( screenDef )
1021+ const diags = inspected . diagnostics . filter ( d => d . code === "orphaned-flow" )
1022+ expect ( diags ) . toHaveLength ( 1 )
1023+ expect ( diags [ 0 ] ?. severity ) . toBe ( "warning" )
1024+ expect ( diags [ 0 ] ?. code ) . toBe ( "orphaned-flow" )
1025+ expect ( diags [ 0 ] ?. message ) . toBe ( '"login" has no surfaced steps.' )
1026+ } )
1027+
1028+ it ( "does not emit orphaned-flow when at least one flow step is surfaced" , ( ) => {
1029+ const screenDef = screen ( "OneStepSurfaced" , $ => {
1030+ const email = $ . state . text ( "email" )
1031+ const pwd = $ . state . text ( "password" )
1032+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
1033+ const pwdAsk = $ . ask ( "Password" , pwd ) . private ( )
1034+ const login = $ . act ( "Log in" )
1035+ $ . surface ( "main" ) . contains ( emailAsk , login )
1036+ $ . flow ( "login" ) . startsWith ( emailAsk ) . then ( pwdAsk ) . then ( login )
1037+ } )
1038+ const inspected = inspectScreen ( screenDef )
1039+ const diags = inspected . diagnostics . filter ( d => d . code === "orphaned-flow" )
1040+ expect ( diags ) . toHaveLength ( 0 )
1041+ } )
1042+
1043+ it ( "multiple flows report only orphaned flows" , ( ) => {
1044+ const screenDef = screen ( "MultipleFlowsOrphan" , $ => {
1045+ const email = $ . state . text ( "email" )
1046+ const pwd = $ . state . text ( "password" )
1047+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
1048+ const pwdAsk = $ . ask ( "Password" , pwd ) . private ( )
1049+ const login = $ . act ( "Log in" )
1050+ const signup = $ . act ( "Sign up" )
1051+ $ . surface ( "main" ) . contains ( emailAsk , login )
1052+ $ . flow ( "a" ) . startsWith ( emailAsk ) . then ( login )
1053+ $ . flow ( "b" ) . startsWith ( pwdAsk ) . then ( signup )
1054+ } )
1055+ const inspected = inspectScreen ( screenDef )
1056+ const diags = inspected . diagnostics . filter ( d => d . code === "orphaned-flow" )
1057+ expect ( diags ) . toHaveLength ( 1 )
1058+ expect ( diags [ 0 ] ?. message ) . toBe ( '"b" has no surfaced steps.' )
1059+ } )
1060+
1061+ it ( "diagnostic includes nested flow.flowNodeId and flow.flowSemanticNodeId" , ( ) => {
1062+ const screenDef = screen ( "OrphanedFlowMeta" , $ => {
1063+ const email = $ . state . text ( "email" )
1064+ const pwd = $ . state . text ( "password" )
1065+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
1066+ const pwdAsk = $ . ask ( "Password" , pwd ) . private ( )
1067+ const login = $ . act ( "Log in" )
1068+ $ . surface ( "main" ) . contains ( login )
1069+ $ . flow ( "login" ) . startsWith ( emailAsk ) . then ( pwdAsk )
1070+ } )
1071+ const inspected = inspectScreen ( screenDef )
1072+ const diags = inspected . diagnostics . filter ( d => d . code === "orphaned-flow" )
1073+ expect ( diags ) . toHaveLength ( 1 )
1074+ expect ( diags [ 0 ] ?. flow ?. flowNodeId ) . toBe ( "flow_login" )
1075+ expect ( diags [ 0 ] ?. flow ?. flowSemanticNodeId ) . toBe ( "flow:login" )
1076+ } )
1077+
1078+ it ( "diagnostic leaves nodeId and semanticNodeId undefined" , ( ) => {
1079+ const screenDef = screen ( "OrphanedNoNodeId" , $ => {
1080+ const email = $ . state . text ( "email" )
1081+ const pwd = $ . state . text ( "password" )
1082+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
1083+ const pwdAsk = $ . ask ( "Password" , pwd ) . private ( )
1084+ const login = $ . act ( "Log in" )
1085+ $ . surface ( "main" ) . contains ( login )
1086+ $ . flow ( "login" ) . startsWith ( emailAsk ) . then ( pwdAsk )
1087+ } )
1088+ const inspected = inspectScreen ( screenDef )
1089+ const diags = inspected . diagnostics . filter ( d => d . code === "orphaned-flow" )
1090+ expect ( diags ) . toHaveLength ( 1 )
1091+ expect ( diags [ 0 ] ?. nodeId ) . toBeUndefined ( )
1092+ expect ( diags [ 0 ] ?. semanticNodeId ) . toBeUndefined ( )
1093+ } )
1094+
1095+ it ( "deterministic ordering across repeated inspectScreen calls" , ( ) => {
1096+ const screenDef = screen ( "OrphanedDeterministic" , $ => {
1097+ const email = $ . state . text ( "email" )
1098+ const pwd = $ . state . text ( "password" )
1099+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
1100+ const pwdAsk = $ . ask ( "Password" , pwd ) . private ( )
1101+ const login = $ . act ( "Log in" )
1102+ const signup = $ . act ( "Sign up" )
1103+ $ . surface ( "main" ) . contains ( login )
1104+ $ . flow ( "a" ) . startsWith ( emailAsk ) . then ( login )
1105+ $ . flow ( "b" ) . startsWith ( pwdAsk ) . then ( signup )
1106+ } )
1107+ const first = inspectScreen ( screenDef )
1108+ const second = inspectScreen ( screenDef )
1109+ expect ( first . diagnostics . map ( d => d . code ) ) . toEqual ( second . diagnostics . map ( d => d . code ) )
1110+ } )
1111+
1112+ it ( "coexists with flow-step-not-surfaced without changing its behavior" , ( ) => {
1113+ const screenDef = screen ( "CoexistOrphan" , $ => {
1114+ const email = $ . state . text ( "email" )
1115+ const pwd = $ . state . text ( "password" )
1116+ const emailAsk = $ . ask ( "Email" , email ) . private ( )
1117+ const pwdAsk = $ . ask ( "Password" , pwd ) . private ( )
1118+ const login = $ . act ( "Log in" )
1119+ const signup = $ . act ( "Sign up" )
1120+ $ . surface ( "main" ) . contains ( login )
1121+ $ . flow ( "a" ) . startsWith ( emailAsk ) . then ( login )
1122+ $ . flow ( "b" ) . startsWith ( pwdAsk ) . then ( signup )
1123+ } )
1124+ const inspected = inspectScreen ( screenDef )
1125+ const orphaned = inspected . diagnostics . filter ( d => d . code === "orphaned-flow" )
1126+ expect ( orphaned ) . toHaveLength ( 1 )
1127+ expect ( orphaned [ 0 ] ?. message ) . toBe ( '"b" has no surfaced steps.' )
1128+ const flowStepNotSurfaced = inspected . diagnostics . filter ( d => d . code === "flow-step-not-surfaced" )
1129+ expect ( flowStepNotSurfaced ) . toHaveLength ( 3 )
1130+ expect ( flowStepNotSurfaced [ 0 ] ?. nodeId ) . toBe ( "ask_email" )
1131+ expect ( flowStepNotSurfaced [ 1 ] ?. nodeId ) . toBe ( "ask_password" )
1132+ expect ( flowStepNotSurfaced [ 2 ] ?. nodeId ) . toBe ( "act_sign_up" )
1133+ } )
1134+ } )
9831135} )
9841136
9851137describe ( "resource" , ( ) => {
0 commit comments