@@ -751,3 +751,333 @@ fn test_dev_hold_across_execute_requests() {
751751 frontend. recv_iopub_idle ( ) ;
752752 frontend. recv_shell_execute_reply ( ) ;
753753}
754+
755+ // Positron-path plot tests (dynamic plots via comm channels)
756+ //
757+ // These tests connect the UI comm to enable the Positron plot path
758+ // (comm-based dynamic plots) instead of the Jupyter protocol path
759+ // (display_data / update_display_data).
760+ //
761+ // In the Positron path:
762+ // - New plots open a "positron.plot" comm (via CommEvent::Opened through
763+ // Shell, arriving on IOPub after idle).
764+ // - Plot updates send a comm_msg directly on IOPub (arriving before idle).
765+ //
766+ // Regression tests for https://github.com/posit-dev/ark/pull/1100
767+
768+ /// Positron path: a single plot opens a plot comm.
769+ #[ test]
770+ fn test_positron_simple_plot ( ) {
771+ let frontend = DummyArkFrontend :: lock ( ) ;
772+ frontend. open_ui_comm ( ) ;
773+ frontend. set_buffer_ui_events ( true ) ;
774+
775+ frontend. send_execute_request ( "plot(1:10)" , ExecuteRequestOptions :: default ( ) ) ;
776+ frontend. recv_iopub_busy ( ) ;
777+ frontend. recv_iopub_execute_input ( ) ;
778+ frontend. recv_iopub_idle ( ) ;
779+ frontend. recv_shell_execute_reply ( ) ;
780+
781+ let open = frontend. recv_iopub_comm_open ( ) ;
782+ assert_eq ! ( open. target_name, "positron.plot" ) ;
783+ }
784+
785+ /// Positron path: two plots in a single request each open their own comm.
786+ #[ test]
787+ fn test_positron_multiple_plots ( ) {
788+ let frontend = DummyArkFrontend :: lock ( ) ;
789+ frontend. open_ui_comm ( ) ;
790+ frontend. set_buffer_ui_events ( true ) ;
791+
792+ frontend. send_execute_request ( "plot(1:10)\n plot(2:20)" , ExecuteRequestOptions :: default ( ) ) ;
793+ frontend. recv_iopub_busy ( ) ;
794+ frontend. recv_iopub_execute_input ( ) ;
795+ frontend. recv_iopub_idle ( ) ;
796+ frontend. recv_shell_execute_reply ( ) ;
797+
798+ let open1 = frontend. recv_iopub_comm_open ( ) ;
799+ let open2 = frontend. recv_iopub_comm_open ( ) ;
800+ assert_eq ! ( open1. target_name, "positron.plot" ) ;
801+ assert_eq ! ( open2. target_name, "positron.plot" ) ;
802+ assert_ne ! ( open1. comm_id, open2. comm_id) ;
803+ }
804+
805+ /// Positron path: `par(mfrow)` creates one plot comm with panel updates.
806+ ///
807+ /// The first panel opens a plot comm; the second panel sends an update
808+ /// on the same comm.
809+ #[ test]
810+ fn test_positron_par_multi_panel ( ) {
811+ let frontend = DummyArkFrontend :: lock ( ) ;
812+ frontend. open_ui_comm ( ) ;
813+ frontend. set_buffer_ui_events ( true ) ;
814+
815+ let code = "par(mfrow = c(2, 1))\n plot(1:10)\n plot(2:20)" ;
816+ frontend. send_execute_request ( code, ExecuteRequestOptions :: default ( ) ) ;
817+ frontend. recv_iopub_busy ( ) ;
818+ frontend. recv_iopub_execute_input ( ) ;
819+
820+ // Panel update arrives directly on IOPub (before idle)
821+ let update = frontend. recv_iopub_comm_msg ( ) ;
822+
823+ frontend. recv_iopub_idle ( ) ;
824+ frontend. recv_shell_execute_reply ( ) ;
825+
826+ // New plot comm arrives through Shell (after idle)
827+ let open = frontend. recv_iopub_comm_open ( ) ;
828+ assert_eq ! ( open. target_name, "positron.plot" ) ;
829+ // Update and open reference the same plot
830+ assert_eq ! ( update. comm_id, open. comm_id) ;
831+ }
832+
833+ /// Positron path: `layout()` + multi-plot works like `par(mfrow)`.
834+ ///
835+ /// Regression: https://github.com/posit-dev/ark/pull/1100#discussion_r2942816670
836+ #[ test]
837+ fn test_positron_layout_multi_plot ( ) {
838+ let frontend = DummyArkFrontend :: lock ( ) ;
839+ frontend. open_ui_comm ( ) ;
840+ frontend. set_buffer_ui_events ( true ) ;
841+
842+ let code = r#"
843+ plt2 = function() {
844+ layout(matrix(1:2, 2))
845+ plot(1, 1)
846+ plot(1, 1)
847+ }
848+ plt2()
849+ "# ;
850+ frontend. send_execute_request ( code, ExecuteRequestOptions :: default ( ) ) ;
851+ frontend. recv_iopub_busy ( ) ;
852+ frontend. recv_iopub_execute_input ( ) ;
853+
854+ // Second panel update (direct IOPub)
855+ let update = frontend. recv_iopub_comm_msg ( ) ;
856+
857+ frontend. recv_iopub_idle ( ) ;
858+ frontend. recv_shell_execute_reply ( ) ;
859+
860+ // New plot comm (through Shell)
861+ let open = frontend. recv_iopub_comm_open ( ) ;
862+ assert_eq ! ( open. target_name, "positron.plot" ) ;
863+ assert_eq ! ( update. comm_id, open. comm_id) ;
864+ }
865+
866+ /// Positron path: `dev.hold()` suppresses intermediate plot output.
867+ ///
868+ /// Only the final state after `dev.flush()` produces a plot comm.
869+ #[ test]
870+ fn test_positron_dev_hold_suppresses ( ) {
871+ let frontend = DummyArkFrontend :: lock ( ) ;
872+ frontend. open_ui_comm ( ) ;
873+ frontend. set_buffer_ui_events ( true ) ;
874+
875+ let code = r#"
876+ invisible(dev.hold())
877+ plot(1:5)
878+ plot(1:3)
879+ invisible(dev.flush())
880+ "# ;
881+ frontend. send_execute_request ( code, ExecuteRequestOptions :: default ( ) ) ;
882+ frontend. recv_iopub_busy ( ) ;
883+ frontend. recv_iopub_execute_input ( ) ;
884+ frontend. recv_iopub_idle ( ) ;
885+ frontend. recv_shell_execute_reply ( ) ;
886+
887+ // Only one plot comm for the final state
888+ let open = frontend. recv_iopub_comm_open ( ) ;
889+ assert_eq ! ( open. target_name, "positron.plot" ) ;
890+ }
891+
892+ /// Positron path: `dev.hold()` persists across execute requests.
893+ ///
894+ /// A hold started in one request should suppress output until
895+ /// `dev.flush()` is called in a subsequent request.
896+ #[ test]
897+ fn test_positron_dev_hold_across_requests ( ) {
898+ let frontend = DummyArkFrontend :: lock ( ) ;
899+ frontend. open_ui_comm ( ) ;
900+ frontend. set_buffer_ui_events ( true ) ;
901+
902+ // Hold and plot without flushing. No plot comm should open.
903+ frontend. send_execute_request (
904+ "invisible(dev.hold())\n plot(1:5)" ,
905+ ExecuteRequestOptions :: default ( ) ,
906+ ) ;
907+ frontend. recv_iopub_busy ( ) ;
908+ frontend. recv_iopub_execute_input ( ) ;
909+ frontend. recv_iopub_idle ( ) ;
910+ frontend. recv_shell_execute_reply ( ) ;
911+
912+ // Flush in a separate request. The held plot should now appear.
913+ frontend. send_execute_request ( "invisible(dev.flush())" , ExecuteRequestOptions :: default ( ) ) ;
914+ frontend. recv_iopub_busy ( ) ;
915+ frontend. recv_iopub_execute_input ( ) ;
916+ frontend. recv_iopub_idle ( ) ;
917+ frontend. recv_shell_execute_reply ( ) ;
918+
919+ let open = frontend. recv_iopub_comm_open ( ) ;
920+ assert_eq ! ( open. target_name, "positron.plot" ) ;
921+ }
922+
923+ /// Positron path: three separate requests each produce a plot comm.
924+ ///
925+ /// Simulates running different packages (e.g. rpart, sf, rpart) one at a
926+ /// time, each producing their own plot.
927+ ///
928+ /// Regression: https://github.com/posit-dev/ark/pull/1100#discussion_r2942842898
929+ #[ test]
930+ fn test_positron_sequential_plots ( ) {
931+ let frontend = DummyArkFrontend :: lock ( ) ;
932+ frontend. open_ui_comm ( ) ;
933+ frontend. set_buffer_ui_events ( true ) ;
934+
935+ for i in 1 ..=3 {
936+ let code = format ! ( "plot({i}:10)" ) ;
937+ frontend. send_execute_request ( & code, ExecuteRequestOptions :: default ( ) ) ;
938+ frontend. recv_iopub_busy ( ) ;
939+ frontend. recv_iopub_execute_input ( ) ;
940+ frontend. recv_iopub_idle ( ) ;
941+ frontend. recv_shell_execute_reply ( ) ;
942+
943+ let open = frontend. recv_iopub_comm_open ( ) ;
944+ assert_eq ! ( open. target_name, "positron.plot" ) ;
945+ }
946+ }
947+
948+ /// Positron path: switching to `png()` and back preserves our plot.
949+ ///
950+ /// The png device is separate from the positron device and should not
951+ /// produce plot comms.
952+ #[ test]
953+ fn test_positron_graphics_device_swap ( ) {
954+ let frontend = DummyArkFrontend :: lock ( ) ;
955+ frontend. open_ui_comm ( ) ;
956+ frontend. set_buffer_ui_events ( true ) ;
957+
958+ let code = r#"
959+ plot(1:10)
960+ grDevices::png(tempfile(fileext = ".png"))
961+ plot(1:20)
962+ invisible(dev.off())
963+ "# ;
964+ frontend. send_execute_request ( code, ExecuteRequestOptions :: default ( ) ) ;
965+ frontend. recv_iopub_busy ( ) ;
966+ frontend. recv_iopub_execute_input ( ) ;
967+ frontend. recv_iopub_idle ( ) ;
968+ frontend. recv_shell_execute_reply ( ) ;
969+
970+ // Only one plot comm for the first plot (on our device)
971+ let open = frontend. recv_iopub_comm_open ( ) ;
972+ assert_eq ! ( open. target_name, "positron.plot" ) ;
973+ }
974+
975+ /// Positron path: plotting in a loop produces one comm per iteration.
976+ #[ test]
977+ fn test_positron_loop_plots ( ) {
978+ let frontend = DummyArkFrontend :: lock ( ) ;
979+ frontend. open_ui_comm ( ) ;
980+ frontend. set_buffer_ui_events ( true ) ;
981+
982+ let code = r#"
983+ for (i in 1:3) {
984+ plot(i)
985+ }
986+ "# ;
987+ frontend. send_execute_request ( code, ExecuteRequestOptions :: default ( ) ) ;
988+ frontend. recv_iopub_busy ( ) ;
989+ frontend. recv_iopub_execute_input ( ) ;
990+ frontend. recv_iopub_idle ( ) ;
991+ frontend. recv_shell_execute_reply ( ) ;
992+
993+ let open1 = frontend. recv_iopub_comm_open ( ) ;
994+ let open2 = frontend. recv_iopub_comm_open ( ) ;
995+ let open3 = frontend. recv_iopub_comm_open ( ) ;
996+ assert_eq ! ( open1. target_name, "positron.plot" ) ;
997+ assert_eq ! ( open2. target_name, "positron.plot" ) ;
998+ assert_eq ! ( open3. target_name, "positron.plot" ) ;
999+ }
1000+
1001+ /// Positron path: `par(mfrow)` with 4 plots in a 3-panel layout.
1002+ ///
1003+ /// The first 3 plots fill the layout (1 new + 2 updates on the same comm).
1004+ /// The 4th plot overflows to a new page, opening a second comm.
1005+ #[ test]
1006+ fn test_positron_par_overflow_to_new_page ( ) {
1007+ let frontend = DummyArkFrontend :: lock ( ) ;
1008+ frontend. open_ui_comm ( ) ;
1009+ frontend. set_buffer_ui_events ( true ) ;
1010+
1011+ let code = r#"
1012+ par(mfrow = c(3, 1))
1013+ plot(1:3)
1014+ plot(4:6)
1015+ plot(7:9)
1016+ plot(10:12)
1017+ par(mfrow = c(1, 1))
1018+ "# ;
1019+ frontend. send_execute_request ( code, ExecuteRequestOptions :: default ( ) ) ;
1020+ frontend. recv_iopub_busy ( ) ;
1021+ frontend. recv_iopub_execute_input ( ) ;
1022+
1023+ // Panels 2 and 3 update the first page (direct IOPub, before idle)
1024+ let update1 = frontend. recv_iopub_comm_msg ( ) ;
1025+ let update2 = frontend. recv_iopub_comm_msg ( ) ;
1026+
1027+ frontend. recv_iopub_idle ( ) ;
1028+ frontend. recv_shell_execute_reply ( ) ;
1029+
1030+ // First page comm (3-panel layout)
1031+ let open1 = frontend. recv_iopub_comm_open ( ) ;
1032+ assert_eq ! ( open1. target_name, "positron.plot" ) ;
1033+
1034+ // Second page comm (4th plot overflows to a new page)
1035+ let open2 = frontend. recv_iopub_comm_open ( ) ;
1036+ assert_eq ! ( open2. target_name, "positron.plot" ) ;
1037+ assert_ne ! ( open1. comm_id, open2. comm_id) ;
1038+
1039+ // Updates for panels 2 and 3 reference the first page
1040+ assert_eq ! ( update1. comm_id, open1. comm_id) ;
1041+ assert_eq ! ( update2. comm_id, open1. comm_id) ;
1042+ }
1043+
1044+ /// Positron path: `dev.hold()` / `dev.flush()` run one line at a time.
1045+ ///
1046+ /// Each line is a separate execute request, simulating interactive use.
1047+ #[ test]
1048+ fn test_positron_dev_hold_flush_interactive ( ) {
1049+ let frontend = DummyArkFrontend :: lock ( ) ;
1050+ frontend. open_ui_comm ( ) ;
1051+ frontend. set_buffer_ui_events ( true ) ;
1052+
1053+ // Hold
1054+ frontend. send_execute_request ( "invisible(dev.hold())" , ExecuteRequestOptions :: default ( ) ) ;
1055+ frontend. recv_iopub_busy ( ) ;
1056+ frontend. recv_iopub_execute_input ( ) ;
1057+ frontend. recv_iopub_idle ( ) ;
1058+ frontend. recv_shell_execute_reply ( ) ;
1059+
1060+ // Draw first plot (held, no comm should open)
1061+ frontend. send_execute_request ( "plot(1:10)" , ExecuteRequestOptions :: default ( ) ) ;
1062+ frontend. recv_iopub_busy ( ) ;
1063+ frontend. recv_iopub_execute_input ( ) ;
1064+ frontend. recv_iopub_idle ( ) ;
1065+ frontend. recv_shell_execute_reply ( ) ;
1066+
1067+ // Draw over it (still held)
1068+ frontend. send_execute_request ( "abline(1, 2)" , ExecuteRequestOptions :: default ( ) ) ;
1069+ frontend. recv_iopub_busy ( ) ;
1070+ frontend. recv_iopub_execute_input ( ) ;
1071+ frontend. recv_iopub_idle ( ) ;
1072+ frontend. recv_shell_execute_reply ( ) ;
1073+
1074+ // Flush - the combined plot should now appear
1075+ frontend. send_execute_request ( "invisible(dev.flush())" , ExecuteRequestOptions :: default ( ) ) ;
1076+ frontend. recv_iopub_busy ( ) ;
1077+ frontend. recv_iopub_execute_input ( ) ;
1078+ frontend. recv_iopub_idle ( ) ;
1079+ frontend. recv_shell_execute_reply ( ) ;
1080+
1081+ let open = frontend. recv_iopub_comm_open ( ) ;
1082+ assert_eq ! ( open. target_name, "positron.plot" ) ;
1083+ }
0 commit comments