Skip to main content

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}