@@ -21,8 +21,8 @@ let unwrap_ok = function Ok v -> v | Error msg -> Alcotest.fail (Printf.sprintf
2121let unwrap_error = function Error msg -> msg | Ok _ -> Alcotest. fail " expected Error but got Ok"
2222
2323(* Assert that decodeReply returns Error with a message starting with expected_prefix *)
24- let assert_decodeReply_errors input expected_prefix () =
25- match ReactServerDOM. decodeReply input with
24+ let assert_decodeReply_errors ? temporaryReferences input expected_prefix () =
25+ match ReactServerDOM. decodeReply ?temporaryReferences input with
2626 | Ok _ -> Alcotest. fail (Printf. sprintf " expected Error starting with %S" expected_prefix)
2727 | Error msg ->
2828 if not (String. starts_with ~prefix: expected_prefix msg) then
@@ -328,6 +328,33 @@ let decodeFormDataReply_hex_id () =
328328 | [| `List [ `String s ] |] -> assert_string s " from_hex"
329329 | _ -> Alcotest. fail " expected hex ID 'a' to resolve to FormData key '10'"
330330
331+ (* Blob ($B) resolution *)
332+
333+ let decodeFormDataReply_blob () =
334+ let formData = Js.FormData. make () in
335+ Js.FormData. append formData " 1" (`String " blob-content-here" );
336+ Js.FormData. append formData " 0" (`String " [\" $B1\" ]" );
337+ let args, _ = ReactServerDOM. decodeFormDataReply formData |> unwrap_ok in
338+ match args with
339+ | [| `String data |] -> assert_string data " blob-content-here"
340+ | _ -> Alcotest. fail " expected blob reference to resolve from FormData"
341+
342+ let decodeReply_blob_without_formdata () =
343+ match ReactServerDOM. decodeReply {| [" $B1" ]| } with
344+ | Error msg ->
345+ if not (String. starts_with ~prefix: " decodeReply: Blob ($B) requires FormData" msg) then
346+ Alcotest. fail (Printf. sprintf " expected FormData error, got %S" msg)
347+ | Ok _ -> Alcotest. fail " expected Error for blob without FormData"
348+
349+ let decodeFormDataReply_blob_missing_entry () =
350+ let formData = Js.FormData. make () in
351+ Js.FormData. append formData " 0" (`String " [\" $B1\" ]" );
352+ match ReactServerDOM. decodeFormDataReply formData with
353+ | Error msg ->
354+ if not (String. starts_with ~prefix: " decodeReply: Blob ($B) entry not found in FormData for key 1" msg) then
355+ Alcotest. fail (Printf. sprintf " expected missing entry error, got %S" msg)
356+ | Ok _ -> Alcotest. fail " expected Error for blob with missing FormData entry"
357+
331358(* Recursive resolution of nested JSON objects *)
332359
333360let decodeReply_nested_special_values_in_object () =
@@ -350,6 +377,121 @@ let decodeReply_nested_special_values_in_array () =
350377 assert_float_is_infinity inf_val
351378 | _ -> Alcotest. fail " expected special values in nested arrays to be resolved"
352379
380+ (* Temporary Reference ($T) tests *)
381+
382+ let decodeReply_temporary_reference_resolves () =
383+ let temporaryReferences = function "abc" -> Some (`String " resolved_value" ) | _ -> None in
384+ let response = ReactServerDOM. decodeReply ~temporary References {| [" $Tabc" ]| } |> unwrap_ok in
385+ match response with
386+ | [| `String s |] -> assert_string s " resolved_value"
387+ | _ -> Alcotest. fail " expected temporary reference to resolve to stored value"
388+
389+ let decodeReply_temporary_reference_not_found () =
390+ let temporaryReferences = function _ -> None in
391+ match ReactServerDOM. decodeReply ~temporary References {| [" $Txyz" ]| } with
392+ | Error msg ->
393+ if not (String. starts_with ~prefix: " decodeReply: Temporary Reference $Txyz not found" msg) then
394+ Alcotest. fail (Printf. sprintf " unexpected error message: %S" msg)
395+ | Ok _ -> Alcotest. fail " expected Error for missing temporary reference"
396+
397+ let decodeReply_temporary_reference_no_resolver () =
398+ match ReactServerDOM. decodeReply {| [" $Tabc" ]| } with
399+ | Error msg ->
400+ if not (String. starts_with ~prefix: " decodeReply: Temporary Reference ($T) requires" msg) then
401+ Alcotest. fail (Printf. sprintf " unexpected error message: %S" msg)
402+ | Ok _ -> Alcotest. fail " expected Error when no temporaryReferences resolver provided"
403+
404+ let decodeReply_temporary_reference_complex_value () =
405+ let temporaryReferences = function
406+ | "obj1" -> Some (`Assoc [ (" key" , `String " value" ); (" num" , `Int 42 ) ])
407+ | _ -> None
408+ in
409+ let response = ReactServerDOM. decodeReply ~temporary References {| [" $Tobj1" ]| } |> unwrap_ok in
410+ match response with
411+ | [| `Assoc [ (" key" , `String v); (" num" , `Int n) ] |] ->
412+ assert_string v " value" ;
413+ assert_int n 42
414+ | _ -> Alcotest. fail " expected temporary reference to resolve to complex value"
415+
416+ let decodeReply_temporary_reference_in_nested_array () =
417+ let temporaryReferences = function "ref1" -> Some (`String " nested_resolved" ) | _ -> None in
418+ let response = ReactServerDOM. decodeReply ~temporary References {| [[" $Tref1" , 42 ]]| } |> unwrap_ok in
419+ match response with
420+ | [| `List [ `String s; `Int n ] |] ->
421+ assert_string s " nested_resolved" ;
422+ assert_int n 42
423+ | _ -> Alcotest. fail " expected temporary reference to resolve inside nested array"
424+
425+ let decodeReply_temporary_reference_in_nested_object () =
426+ let temporaryReferences = function "ref1" -> Some (`String " obj_resolved" ) | _ -> None in
427+ let response = ReactServerDOM. decodeReply ~temporary References {| [{" val" : " $Tref1" , " other" : 1 }]| } |> unwrap_ok in
428+ match response with
429+ | [| `Assoc [ (" val" , `String s); (" other" , `Int n) ] |] ->
430+ assert_string s " obj_resolved" ;
431+ assert_int n 1
432+ | _ -> Alcotest. fail " expected temporary reference to resolve inside nested object"
433+
434+ (* decodeAction tests *)
435+
436+ let decodeAction_with_action_id_and_fields () =
437+ let formData = Js.FormData. make () in
438+ Js.FormData. append formData " $ACTION_ID_abc123" (`String " " );
439+ Js.FormData. append formData " name" (`String " Lola" );
440+ Js.FormData. append formData " age" (`String " 20" );
441+ match ReactServerDOM. decodeAction formData with
442+ | Some (id , user_fd ) -> (
443+ assert_string id " abc123" ;
444+ match (Js.FormData. get user_fd " name" , Js.FormData. get user_fd " age" ) with
445+ | `String name , `String age ->
446+ assert_string name " Lola" ;
447+ assert_string age " 20" )
448+ | None -> Alcotest. fail " expected Some but got None"
449+
450+ let decodeAction_no_action_keys () =
451+ let formData = Js.FormData. make () in
452+ Js.FormData. append formData " name" (`String " Lola" );
453+ Js.FormData. append formData " age" (`String " 20" );
454+ match ReactServerDOM. decodeAction formData with None -> () | Some _ -> Alcotest. fail " expected None but got Some"
455+
456+ let decodeAction_action_id_only () =
457+ let formData = Js.FormData. make () in
458+ Js.FormData. append formData " $ACTION_ID_abc123" (`String " " );
459+ match ReactServerDOM. decodeAction formData with
460+ | Some (id , user_fd ) ->
461+ assert_string id " abc123" ;
462+ let entries = Js.FormData. entries user_fd in
463+ Alcotest. check Alcotest. int " should have 0 user entries" (List. length entries) 0
464+ | None -> Alcotest. fail " expected Some but got None"
465+
466+ let decodeAction_multiple_action_keys () =
467+ let formData = Js.FormData. make () in
468+ Js.FormData. append formData " $ACTION_ID_first" (`String " " );
469+ Js.FormData. append formData " name" (`String " Lola" );
470+ Js.FormData. append formData " $ACTION_ID_second" (`String " " );
471+ match ReactServerDOM. decodeAction formData with
472+ | Some (id , user_fd ) -> (
473+ (* Either action ID is valid since Hashtbl iteration order is unspecified *)
474+ assert_bool (String. equal id " first" || String. equal id " second" ) true ;
475+ match Js.FormData. get user_fd " name" with `String name -> assert_string name " Lola" )
476+ | None -> Alcotest. fail " expected Some but got None"
477+
478+ let decodeAction_filters_other_action_keys () =
479+ (* $ACTION_REF_ and other $ACTION_ prefixed keys should be filtered out from user data *)
480+ let formData = Js.FormData. make () in
481+ Js.FormData. append formData " $ACTION_ID_abc123" (`String " " );
482+ Js.FormData. append formData " $ACTION_REF_xyz" (`String " some_ref" );
483+ Js.FormData. append formData " name" (`String " Lola" );
484+ match ReactServerDOM. decodeAction formData with
485+ | Some (id , user_fd ) -> (
486+ assert_string id " abc123" ;
487+ (match Js.FormData. get user_fd " name" with `String name -> assert_string name " Lola" );
488+ (* $ACTION_REF_ should not be in user_fd *)
489+ try
490+ let _ = Js.FormData. get user_fd " $ACTION_REF_xyz" in
491+ Alcotest. fail " $ACTION_REF_ key should not be in user FormData"
492+ with Not_found -> () )
493+ | None -> Alcotest. fail " expected Some but got None"
494+
353495let test title fn = (Printf. sprintf " Decoders / %s" title, [ Alcotest_lwt. test_case_sync " " `Quick fn ])
354496
355497let tests =
@@ -402,10 +544,18 @@ let tests =
402544 test " decodeReply: $W Set without FormData raises" (assert_decodeReply_errors " [\" $W1\" ]" " decodeReply: Set" );
403545 (* Unsupported types raise descriptive errors *)
404546 test " decodeReply: $@ Promise raises" (assert_decodeReply_errors " [\" $@1\" ]" " decodeReply: Promise" );
405- test " decodeReply: $T Temporary Reference raises"
406- (assert_decodeReply_errors " [\" $T1\" ]" " decodeReply: Temporary Reference" );
547+ (* Temporary References ($T) *)
548+ test " decodeReply: $T resolves with temporaryReferences" decodeReply_temporary_reference_resolves;
549+ test " decodeReply: $T not found returns error" decodeReply_temporary_reference_not_found;
550+ test " decodeReply: $T without resolver returns error" decodeReply_temporary_reference_no_resolver;
551+ test " decodeReply: $T resolves complex value" decodeReply_temporary_reference_complex_value;
552+ test " decodeReply: $T resolves in nested array" decodeReply_temporary_reference_in_nested_array;
553+ test " decodeReply: $T resolves in nested object" decodeReply_temporary_reference_in_nested_object;
407554 test " decodeReply: $A TypedArray raises" (assert_decodeReply_errors " [\" $A1\" ]" " decodeReply: TypedArray" );
408- test " decodeReply: $B Blob raises" (assert_decodeReply_errors " [\" $B1\" ]" " decodeReply: Blob" );
555+ (* Blob ($B) resolution *)
556+ test " decodeFormDataReply: $B Blob resolves from FormData" decodeFormDataReply_blob;
557+ test " decodeReply: $B Blob without FormData returns error" decodeReply_blob_without_formdata;
558+ test " decodeFormDataReply: $B Blob with missing entry returns error" decodeFormDataReply_blob_missing_entry;
409559 test " decodeReply: $R ReadableStream raises"
410560 (assert_decodeReply_errors " [\" $R1\" ]" " decodeReply: ReadableStream ($R)" );
411561 test " decodeReply: $r ReadableStream bytes raises"
@@ -416,4 +566,10 @@ let tests =
416566 List. iter
417567 (fun prefix -> assert_decodeReply_errors (Printf. sprintf " [\" $%s1\" ]" prefix) " decodeReply: TypedArray" () )
418568 [ " O" ; " o" ; " U" ; " S" ; " s" ; " L" ; " l" ; " G" ; " g" ; " M" ; " m" ; " V" ]);
569+ (* decodeAction *)
570+ test " decodeAction: $ACTION_ID with form fields" decodeAction_with_action_id_and_fields;
571+ test " decodeAction: no $ACTION_* keys returns None" decodeAction_no_action_keys;
572+ test " decodeAction: $ACTION_ID with no other fields" decodeAction_action_id_only;
573+ test " decodeAction: multiple $ACTION_ID keys (unspecified which wins)" decodeAction_multiple_action_keys;
574+ test " decodeAction: filters $ACTION_* keys from user data" decodeAction_filters_other_action_keys;
419575 ]
0 commit comments