Skip to main content

cbf_chrome/
command.rs

1use cbf::data::dialog::DialogResponse;
2use cbf::{
3    command::{BrowserCommand, BrowserOperation},
4    data::edit::EditAction,
5};
6
7use crate::data::{
8    browsing_context_open::ChromeBrowsingContextOpenResponse,
9    download::ChromeDownloadId,
10    drag::{ChromeDragDrop, ChromeDragUpdate},
11    extension::ChromeAuxiliaryWindowResponse,
12    ids::{PopupId, TabId},
13    ime::{
14        ChromeConfirmCompositionBehavior, ChromeImeCommitText, ChromeImeComposition,
15        ChromeTransientImeCommitText, ChromeTransientImeComposition,
16    },
17    input::{ChromeKeyEvent, ChromeMouseWheelEvent},
18    mouse::ChromeMouseEvent,
19    prompt_ui::{PromptUiId, PromptUiResponse},
20    window_open::ChromeWindowOpenResponse,
21};
22
23/// Chromium-specific transport command vocabulary.
24///
25/// Transient browsing context operations are currently transported through
26/// Chromium's extension popup plumbing. The public `cbf` API remains generic,
27/// and this layer performs the boundary translation into the current Chrome
28/// implementation model.
29#[derive(Debug, Clone, PartialEq)]
30pub enum ChromeCommand {
31    RequestShutdown {
32        request_id: u64,
33    },
34    ConfirmShutdown {
35        request_id: u64,
36        proceed: bool,
37    },
38    ForceShutdown,
39    ConfirmBeforeUnload {
40        browsing_context_id: TabId,
41        request_id: u64,
42        proceed: bool,
43    },
44    RespondJavaScriptDialog {
45        browsing_context_id: TabId,
46        request_id: u64,
47        response: DialogResponse,
48    },
49    RespondExtensionPopupJavaScriptDialog {
50        popup_id: PopupId,
51        request_id: u64,
52        response: DialogResponse,
53    },
54    ConfirmPermission {
55        browsing_context_id: TabId,
56        request_id: u64,
57        allow: bool,
58    },
59    CreateTab {
60        request_id: u64,
61        initial_url: Option<String>,
62        profile_id: String,
63    },
64    ListProfiles,
65    RequestCloseTab {
66        browsing_context_id: TabId,
67    },
68    SetTabSize {
69        browsing_context_id: TabId,
70        width: u32,
71        height: u32,
72    },
73    Navigate {
74        browsing_context_id: TabId,
75        url: String,
76    },
77    GoBack {
78        browsing_context_id: TabId,
79    },
80    GoForward {
81        browsing_context_id: TabId,
82    },
83    Reload {
84        browsing_context_id: TabId,
85        ignore_cache: bool,
86    },
87    PrintPreview {
88        browsing_context_id: TabId,
89    },
90    OpenDevTools {
91        browsing_context_id: TabId,
92    },
93    InspectElement {
94        browsing_context_id: TabId,
95        x: i32,
96        y: i32,
97    },
98    GetTabDomHtml {
99        browsing_context_id: TabId,
100        request_id: u64,
101    },
102    SetTabFocus {
103        browsing_context_id: TabId,
104        focused: bool,
105    },
106    SendKeyEvent {
107        browsing_context_id: TabId,
108        event: ChromeKeyEvent,
109        commands: Vec<String>,
110    },
111    ExecuteEditAction {
112        browsing_context_id: TabId,
113        action: EditAction,
114    },
115    SendMouseEvent {
116        browsing_context_id: TabId,
117        event: ChromeMouseEvent,
118    },
119    SendMouseWheelEvent {
120        browsing_context_id: TabId,
121        event: ChromeMouseWheelEvent,
122    },
123    SendDragUpdate {
124        update: ChromeDragUpdate,
125    },
126    SendDragDrop {
127        drop: ChromeDragDrop,
128    },
129    SendDragCancel {
130        session_id: u64,
131        browsing_context_id: TabId,
132    },
133    SetImeComposition {
134        composition: ChromeImeComposition,
135    },
136    CommitImeText {
137        commit: ChromeImeCommitText,
138    },
139    FinishComposingText {
140        browsing_context_id: TabId,
141        behavior: ChromeConfirmCompositionBehavior,
142    },
143    ExecuteContextMenuCommand {
144        menu_id: u64,
145        command_id: i32,
146        event_flags: i32,
147    },
148    AcceptChoiceMenuSelection {
149        request_id: u64,
150        indices: Vec<i32>,
151    },
152    DismissChoiceMenu {
153        request_id: u64,
154    },
155    DismissContextMenu {
156        menu_id: u64,
157    },
158    PauseDownload {
159        download_id: ChromeDownloadId,
160    },
161    ResumeDownload {
162        download_id: ChromeDownloadId,
163    },
164    CancelDownload {
165        download_id: ChromeDownloadId,
166    },
167    ListExtensions {
168        profile_id: String,
169    },
170    ActivateExtensionAction {
171        browsing_context_id: TabId,
172        extension_id: String,
173    },
174    CloseExtensionPopup {
175        popup_id: PopupId,
176    },
177    SetExtensionPopupSize {
178        popup_id: PopupId,
179        width: u32,
180        height: u32,
181    },
182    SetExtensionPopupFocus {
183        popup_id: PopupId,
184        focused: bool,
185    },
186    SendExtensionPopupKeyEvent {
187        popup_id: PopupId,
188        event: ChromeKeyEvent,
189        commands: Vec<String>,
190    },
191    ExecuteExtensionPopupEditAction {
192        popup_id: PopupId,
193        action: EditAction,
194    },
195    SendExtensionPopupMouseEvent {
196        popup_id: PopupId,
197        event: ChromeMouseEvent,
198    },
199    SendExtensionPopupMouseWheelEvent {
200        popup_id: PopupId,
201        event: ChromeMouseWheelEvent,
202    },
203    SetExtensionPopupComposition {
204        composition: ChromeTransientImeComposition,
205    },
206    CommitExtensionPopupText {
207        commit: ChromeTransientImeCommitText,
208    },
209    FinishExtensionPopupComposingText {
210        popup_id: PopupId,
211        behavior: ChromeConfirmCompositionBehavior,
212    },
213    OpenDefaultPromptUi {
214        profile_id: String,
215        request_id: u64,
216    },
217    RespondPromptUi {
218        profile_id: String,
219        request_id: u64,
220        response: PromptUiResponse,
221    },
222    ClosePromptUi {
223        profile_id: String,
224        prompt_ui_id: PromptUiId,
225    },
226    RespondTabOpen {
227        request_id: u64,
228        response: ChromeBrowsingContextOpenResponse,
229    },
230    RespondWindowOpen {
231        request_id: u64,
232        response: ChromeWindowOpenResponse,
233    },
234    UnsupportedGenericCommand {
235        operation: BrowserOperation,
236    },
237}
238
239impl From<BrowserCommand> for ChromeCommand {
240    fn from(value: BrowserCommand) -> Self {
241        match value {
242            BrowserCommand::Shutdown { request_id } => Self::RequestShutdown { request_id },
243            BrowserCommand::ConfirmShutdown {
244                request_id,
245                proceed,
246            } => Self::ConfirmShutdown {
247                request_id,
248                proceed,
249            },
250            BrowserCommand::ForceShutdown => Self::ForceShutdown,
251            BrowserCommand::ConfirmBeforeUnload {
252                browsing_context_id,
253                request_id,
254                proceed,
255            } => Self::ConfirmBeforeUnload {
256                browsing_context_id: browsing_context_id.into(),
257                request_id,
258                proceed,
259            },
260            BrowserCommand::RespondJavaScriptDialog {
261                browsing_context_id,
262                request_id,
263                response,
264            } => Self::RespondJavaScriptDialog {
265                browsing_context_id: browsing_context_id.into(),
266                request_id,
267                response,
268            },
269            BrowserCommand::RespondJavaScriptDialogInTransientBrowsingContext {
270                transient_browsing_context_id,
271                request_id,
272                response,
273            } => Self::RespondExtensionPopupJavaScriptDialog {
274                popup_id: transient_browsing_context_id.into(),
275                request_id,
276                response,
277            },
278            BrowserCommand::ConfirmPermission {
279                browsing_context_id,
280                request_id,
281                allow,
282            } => Self::ConfirmPermission {
283                browsing_context_id: browsing_context_id.into(),
284                request_id,
285                allow,
286            },
287            BrowserCommand::CreateBrowsingContext {
288                request_id,
289                initial_url,
290                profile_id,
291            } => Self::CreateTab {
292                request_id,
293                initial_url,
294                profile_id,
295            },
296            BrowserCommand::ListProfiles => Self::ListProfiles,
297            BrowserCommand::RequestCloseBrowsingContext {
298                browsing_context_id,
299            } => Self::RequestCloseTab {
300                browsing_context_id: browsing_context_id.into(),
301            },
302            BrowserCommand::CloseTransientBrowsingContext {
303                transient_browsing_context_id,
304            } => Self::CloseExtensionPopup {
305                popup_id: transient_browsing_context_id.into(),
306            },
307            BrowserCommand::ResizeBrowsingContext {
308                browsing_context_id,
309                width,
310                height,
311            } => Self::SetTabSize {
312                browsing_context_id: browsing_context_id.into(),
313                width,
314                height,
315            },
316            BrowserCommand::ResizeTransientBrowsingContext {
317                transient_browsing_context_id,
318                width,
319                height,
320            } => Self::SetExtensionPopupSize {
321                popup_id: transient_browsing_context_id.into(),
322                width,
323                height,
324            },
325            BrowserCommand::Navigate {
326                browsing_context_id,
327                url,
328            } => Self::Navigate {
329                browsing_context_id: browsing_context_id.into(),
330                url,
331            },
332            BrowserCommand::GoBack {
333                browsing_context_id,
334            } => Self::GoBack {
335                browsing_context_id: browsing_context_id.into(),
336            },
337            BrowserCommand::GoForward {
338                browsing_context_id,
339            } => Self::GoForward {
340                browsing_context_id: browsing_context_id.into(),
341            },
342            BrowserCommand::Reload {
343                browsing_context_id,
344                ignore_cache,
345            } => Self::Reload {
346                browsing_context_id: browsing_context_id.into(),
347                ignore_cache,
348            },
349            BrowserCommand::PrintPreview {
350                browsing_context_id,
351            } => Self::PrintPreview {
352                browsing_context_id: browsing_context_id.into(),
353            },
354            BrowserCommand::GetBrowsingContextDomHtml {
355                browsing_context_id,
356                request_id,
357            } => Self::GetTabDomHtml {
358                browsing_context_id: browsing_context_id.into(),
359                request_id,
360            },
361            BrowserCommand::SetBrowsingContextFocus {
362                browsing_context_id,
363                focused,
364            } => Self::SetTabFocus {
365                browsing_context_id: browsing_context_id.into(),
366                focused,
367            },
368            BrowserCommand::SetTransientBrowsingContextFocus {
369                transient_browsing_context_id,
370                focused,
371            } => Self::SetExtensionPopupFocus {
372                popup_id: transient_browsing_context_id.into(),
373                focused,
374            },
375            BrowserCommand::SendKeyEvent {
376                browsing_context_id,
377                event,
378                commands,
379            } => Self::SendKeyEvent {
380                browsing_context_id: browsing_context_id.into(),
381                event: event.into(),
382                commands,
383            },
384            BrowserCommand::ExecuteEditAction {
385                browsing_context_id,
386                action,
387            } => Self::ExecuteEditAction {
388                browsing_context_id: browsing_context_id.into(),
389                action,
390            },
391            BrowserCommand::SendKeyEventToTransientBrowsingContext {
392                transient_browsing_context_id,
393                event,
394                commands,
395            } => Self::SendExtensionPopupKeyEvent {
396                popup_id: transient_browsing_context_id.into(),
397                event: event.into(),
398                commands,
399            },
400            BrowserCommand::ExecuteEditActionInTransientBrowsingContext {
401                transient_browsing_context_id,
402                action,
403            } => Self::ExecuteExtensionPopupEditAction {
404                popup_id: transient_browsing_context_id.into(),
405                action,
406            },
407            BrowserCommand::SendMouseEvent {
408                browsing_context_id,
409                event,
410            } => Self::SendMouseEvent {
411                browsing_context_id: browsing_context_id.into(),
412                event: event.into(),
413            },
414            BrowserCommand::SendMouseEventToTransientBrowsingContext {
415                transient_browsing_context_id,
416                event,
417            } => Self::SendExtensionPopupMouseEvent {
418                popup_id: transient_browsing_context_id.into(),
419                event: event.into(),
420            },
421            BrowserCommand::SendMouseWheelEvent {
422                browsing_context_id,
423                event,
424            } => Self::SendMouseWheelEvent {
425                browsing_context_id: browsing_context_id.into(),
426                event: event.into(),
427            },
428            BrowserCommand::SendMouseWheelEventToTransientBrowsingContext {
429                transient_browsing_context_id,
430                event,
431            } => Self::SendExtensionPopupMouseWheelEvent {
432                popup_id: transient_browsing_context_id.into(),
433                event: event.into(),
434            },
435            BrowserCommand::SendDragUpdate { update } => Self::SendDragUpdate {
436                update: update.into(),
437            },
438            BrowserCommand::SendDragDrop { drop } => Self::SendDragDrop { drop: drop.into() },
439            BrowserCommand::SendDragCancel {
440                session_id,
441                browsing_context_id,
442            } => Self::SendDragCancel {
443                session_id,
444                browsing_context_id: browsing_context_id.into(),
445            },
446            BrowserCommand::SetComposition { composition } => Self::SetImeComposition {
447                composition: composition.into(),
448            },
449            BrowserCommand::CommitText { commit } => Self::CommitImeText {
450                commit: commit.into(),
451            },
452            BrowserCommand::SetTransientComposition { composition } => {
453                Self::SetExtensionPopupComposition {
454                    composition: composition.into(),
455                }
456            }
457            BrowserCommand::CommitTransientText { commit } => Self::CommitExtensionPopupText {
458                commit: commit.into(),
459            },
460            BrowserCommand::FinishComposingText {
461                browsing_context_id,
462                behavior,
463            } => Self::FinishComposingText {
464                browsing_context_id: browsing_context_id.into(),
465                behavior: behavior.into(),
466            },
467            BrowserCommand::FinishComposingTextInTransientBrowsingContext {
468                transient_browsing_context_id,
469                behavior,
470            } => Self::FinishExtensionPopupComposingText {
471                popup_id: transient_browsing_context_id.into(),
472                behavior: behavior.into(),
473            },
474            BrowserCommand::ExecuteContextMenuCommand {
475                menu_id,
476                command_id,
477                event_flags,
478            } => Self::ExecuteContextMenuCommand {
479                menu_id,
480                command_id,
481                event_flags,
482            },
483            BrowserCommand::AcceptChoiceMenuSelection {
484                request_id,
485                indices,
486            } => Self::AcceptChoiceMenuSelection {
487                request_id,
488                indices,
489            },
490            BrowserCommand::DismissChoiceMenu { request_id } => {
491                Self::DismissChoiceMenu { request_id }
492            }
493            BrowserCommand::DismissContextMenu { menu_id } => Self::DismissContextMenu { menu_id },
494            BrowserCommand::PauseDownload { download_id } => Self::PauseDownload {
495                download_id: download_id.into(),
496            },
497            BrowserCommand::ResumeDownload { download_id } => Self::ResumeDownload {
498                download_id: download_id.into(),
499            },
500            BrowserCommand::CancelDownload { download_id } => Self::CancelDownload {
501                download_id: download_id.into(),
502            },
503            BrowserCommand::ListExtensions { profile_id } => Self::ListExtensions { profile_id },
504            BrowserCommand::OpenDefaultAuxiliaryWindow {
505                profile_id,
506                request_id,
507            } => Self::OpenDefaultPromptUi {
508                profile_id,
509                request_id,
510            },
511            BrowserCommand::RespondAuxiliaryWindow {
512                profile_id,
513                request_id,
514                response,
515            } => match ChromeAuxiliaryWindowResponse::from(response) {
516                ChromeAuxiliaryWindowResponse::PermissionPrompt { allow } => {
517                    Self::RespondPromptUi {
518                        profile_id,
519                        request_id,
520                        response: PromptUiResponse::PermissionPrompt { allow },
521                    }
522                }
523                ChromeAuxiliaryWindowResponse::DownloadPrompt {
524                    allow,
525                    destination_path,
526                } => Self::RespondPromptUi {
527                    profile_id,
528                    request_id,
529                    response: PromptUiResponse::DownloadPrompt {
530                        allow,
531                        destination_path,
532                    },
533                },
534                ChromeAuxiliaryWindowResponse::ExtensionInstallPrompt { proceed } => {
535                    Self::RespondPromptUi {
536                        profile_id,
537                        request_id,
538                        response: PromptUiResponse::ExtensionInstallPrompt { proceed },
539                    }
540                }
541                ChromeAuxiliaryWindowResponse::ExtensionUninstallPrompt {
542                    proceed,
543                    report_abuse,
544                } => Self::RespondPromptUi {
545                    profile_id,
546                    request_id,
547                    response: PromptUiResponse::ExtensionUninstallPrompt {
548                        proceed,
549                        report_abuse,
550                    },
551                },
552                ChromeAuxiliaryWindowResponse::Unknown => Self::RespondPromptUi {
553                    profile_id,
554                    request_id,
555                    response: PromptUiResponse::Unknown,
556                },
557            },
558            BrowserCommand::CloseAuxiliaryWindow {
559                profile_id,
560                window_id,
561            } => Self::ClosePromptUi {
562                profile_id,
563                prompt_ui_id: PromptUiId::new(window_id.get()),
564            },
565            BrowserCommand::RespondBrowsingContextOpen {
566                request_id,
567                response,
568            } => Self::RespondTabOpen {
569                request_id,
570                response: response.into(),
571            },
572            BrowserCommand::RespondWindowOpen {
573                request_id,
574                response,
575            } => Self::RespondWindowOpen {
576                request_id,
577                response,
578            },
579        }
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    use cbf::{
586        command::BrowserCommand,
587        data::{
588            auxiliary_window::{AuxiliaryWindowId, AuxiliaryWindowResponse},
589            edit::EditAction,
590            ids::{BrowsingContextId, TransientBrowsingContextId},
591        },
592    };
593
594    use super::ChromeCommand;
595    use crate::data::{
596        ids::{PopupId, TabId},
597        prompt_ui::{PromptUiId, PromptUiResponse},
598    };
599
600    #[test]
601    fn create_close_command_converts_browsing_context_id_into_tab_id() {
602        let command = BrowserCommand::RequestCloseBrowsingContext {
603            browsing_context_id: BrowsingContextId::new(42),
604        };
605
606        let raw: ChromeCommand = command.into();
607        assert!(matches!(
608            raw,
609            ChromeCommand::RequestCloseTab {
610                browsing_context_id
611            } if browsing_context_id == TabId::new(42)
612        ));
613    }
614
615    #[test]
616    fn confirm_permission_maps_to_prompt_ui_response() {
617        let command = BrowserCommand::ConfirmPermission {
618            browsing_context_id: BrowsingContextId::new(9),
619            request_id: 77,
620            allow: true,
621        };
622
623        let raw: ChromeCommand = command.into();
624        assert!(matches!(
625            raw,
626            ChromeCommand::ConfirmPermission {
627                browsing_context_id,
628                request_id,
629                allow: true,
630            } if browsing_context_id == TabId::new(9) && request_id == 77
631        ));
632    }
633
634    #[test]
635    fn permission_auxiliary_response_maps_to_prompt_ui_response() {
636        let command = BrowserCommand::RespondAuxiliaryWindow {
637            profile_id: "profile-a".to_string(),
638            request_id: 81,
639            response: AuxiliaryWindowResponse::PermissionPrompt { allow: false },
640        };
641
642        let raw: ChromeCommand = command.into();
643        assert!(matches!(
644            raw,
645            ChromeCommand::RespondPromptUi {
646                profile_id,
647                request_id,
648                response: PromptUiResponse::PermissionPrompt { allow: false },
649            } if profile_id == "profile-a" && request_id == 81
650        ));
651    }
652
653    #[test]
654    fn extension_auxiliary_response_maps_to_prompt_ui_response() {
655        let command = BrowserCommand::RespondAuxiliaryWindow {
656            profile_id: "profile-b".to_string(),
657            request_id: 82,
658            response: AuxiliaryWindowResponse::ExtensionInstallPrompt { proceed: true },
659        };
660
661        let raw: ChromeCommand = command.into();
662        assert!(matches!(
663            raw,
664            ChromeCommand::RespondPromptUi {
665                profile_id,
666                request_id,
667                response: PromptUiResponse::ExtensionInstallPrompt { proceed: true },
668            } if profile_id == "profile-b" && request_id == 82
669        ));
670    }
671
672    #[test]
673    fn uninstall_auxiliary_response_maps_to_prompt_ui_response() {
674        let command = BrowserCommand::RespondAuxiliaryWindow {
675            profile_id: "profile-b".to_string(),
676            request_id: 83,
677            response: AuxiliaryWindowResponse::ExtensionUninstallPrompt {
678                proceed: true,
679                report_abuse: true,
680            },
681        };
682
683        let raw: ChromeCommand = command.into();
684        assert!(matches!(
685            raw,
686            ChromeCommand::RespondPromptUi {
687                profile_id,
688                request_id,
689                response: PromptUiResponse::ExtensionUninstallPrompt {
690                    proceed: true,
691                    report_abuse: true,
692                },
693            } if profile_id == "profile-b" && request_id == 83
694        ));
695    }
696
697    #[test]
698    fn close_auxiliary_window_maps_to_prompt_ui_close() {
699        let command = BrowserCommand::CloseAuxiliaryWindow {
700            profile_id: "profile-c".to_string(),
701            window_id: AuxiliaryWindowId::new(33),
702        };
703
704        let raw: ChromeCommand = command.into();
705        assert!(matches!(
706            raw,
707            ChromeCommand::ClosePromptUi {
708                profile_id,
709                prompt_ui_id,
710            } if profile_id == "profile-c" && prompt_ui_id == PromptUiId::new(33)
711        ));
712    }
713
714    #[test]
715    fn transient_close_command_maps_to_extension_popup_close() {
716        let command = BrowserCommand::CloseTransientBrowsingContext {
717            transient_browsing_context_id: TransientBrowsingContextId::new(99),
718        };
719
720        let raw: ChromeCommand = command.into();
721        assert!(matches!(
722            raw,
723            ChromeCommand::CloseExtensionPopup { popup_id }
724                if popup_id == PopupId::new(99)
725        ));
726    }
727
728    #[test]
729    fn edit_action_command_maps_to_chrome_edit_action() {
730        let command = BrowserCommand::ExecuteEditAction {
731            browsing_context_id: BrowsingContextId::new(11),
732            action: EditAction::Paste,
733        };
734
735        let raw: ChromeCommand = command.into();
736        assert!(matches!(
737            raw,
738            ChromeCommand::ExecuteEditAction {
739                browsing_context_id,
740                action: EditAction::Paste,
741            } if browsing_context_id == TabId::new(11)
742        ));
743    }
744
745    #[test]
746    fn transient_edit_action_command_maps_to_extension_popup_edit_action() {
747        let command = BrowserCommand::ExecuteEditActionInTransientBrowsingContext {
748            transient_browsing_context_id: TransientBrowsingContextId::new(12),
749            action: EditAction::SelectAll,
750        };
751
752        let raw: ChromeCommand = command.into();
753        assert!(matches!(
754            raw,
755            ChromeCommand::ExecuteExtensionPopupEditAction {
756                popup_id,
757                action: EditAction::SelectAll,
758            } if popup_id == PopupId::new(12)
759        ));
760    }
761}