playwright_cdp/file_chooser.rs
1//! `FileChooser` — file-chooser interception via the CDP `Page` domain.
2//!
3//! `Page::on_filechooser(handler)` enables chooser interception once
4//! (`Page.setInterceptFileChooserDialog { enabled: true }`) and dispatches a
5//! [`FileChooser`] for every `Page.fileChooserOpened` event. The handler
6//! inspects the chooser ([`is_multiple`](FileChooser::is_multiple),
7//! [`element`](FileChooser::element)) and accepts files via
8//! [`set_files`](FileChooser::set_files) (`Page.handleFileChooser { action:
9//! "accept", files: [...] }`).
10
11use crate::element_handle::ElementHandle;
12use crate::error::{Error, Result};
13use crate::page::Page;
14use serde_json::{json, Value};
15use std::path::Path;
16
17/// A file-chooser dialog opened by the page.
18///
19/// Produced by [`Page::on_filechooser`](Page::on_filechooser). Call
20/// [`set_files`](FileChooser::set_files) to accept the chooser with the given
21/// server-side file path(s).
22#[derive(Clone)]
23pub struct FileChooser {
24 page: Page,
25 /// The `<input type=file>` that triggered the chooser, if one is known
26 /// (absent for choosers opened via `HTMLInputElement.showOpenFilePicker`
27 /// without a backing element). Resolved lazily via `DOM.resolveNode`.
28 backend_node_id: Option<i64>,
29 multiple: bool,
30}
31
32impl FileChooser {
33 pub(crate) fn new(page: Page, backend_node_id: Option<i64>, multiple: bool) -> Self {
34 Self {
35 page,
36 backend_node_id,
37 multiple,
38 }
39 }
40
41 /// The owning page.
42 pub fn page(&self) -> Page {
43 self.page.clone()
44 }
45
46 /// `true` if the chooser allows selecting multiple files (`selectMultiple`).
47 pub fn is_multiple(&self) -> bool {
48 self.multiple
49 }
50
51 /// The `<input type=file>` element that triggered the chooser, if any.
52 ///
53 /// Resolves the CDP `backendNodeId` from the event to a remote object id via
54 /// `DOM.resolveNode`, then wraps it in an [`ElementHandle`]. Returns `None`
55 /// if the event carried no `backendNodeId` (no backing element).
56 pub async fn element(&self) -> Result<Option<ElementHandle>> {
57 let backend_node_id = match self.backend_node_id {
58 Some(id) => id,
59 None => return Ok(None),
60 };
61 let resp = self
62 .page
63 .session()
64 .send("DOM.resolveNode", json!({ "backendNodeId": backend_node_id }))
65 .await?;
66 // The remote object is under `object.objectId`.
67 let object_id = resp
68 .get("object")
69 .and_then(|o| o.get("objectId"))
70 .and_then(|v| v.as_str())
71 .ok_or_else(|| {
72 Error::ProtocolError("DOM.resolveNode missing object.objectId".into())
73 })?;
74 Ok(Some(ElementHandle::new(self.page.clone(), object_id.to_string())))
75 }
76
77 /// Accept the chooser, uploading the given server-side file path(s).
78 ///
79 /// `files` are absolute filesystem paths Chrome can read directly (not file
80 /// contents). Sets the files on the chooser's `<input type=file>` (resolved
81 /// from the event's `backendNodeId`) via `DOM.setFileInputFiles`, which is
82 /// supported across Chrome versions.
83 pub async fn set_files(&self, files: &[impl AsRef<Path>]) -> Result<()> {
84 let backend_node_id = self.backend_node_id.ok_or_else(|| {
85 Error::ProtocolError("file chooser has no backing input element".into())
86 })?;
87 let paths: Vec<String> = files
88 .iter()
89 .map(|p| p.as_ref().to_string_lossy().into_owned())
90 .collect();
91 self.page
92 .session()
93 .send(
94 "DOM.setFileInputFiles",
95 json!({ "backendNodeId": backend_node_id, "files": paths }),
96 )
97 .await
98 .map(|_: Value| ())
99 }
100}