1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
// DropOptions and related types
//
// Provides configuration for Locator::drop, matching Playwright's API.
use crate::protocol::FilePayload;
use crate::protocol::click::Position;
/// Options for [`Locator::drop()`](crate::protocol::Locator::drop).
///
/// Simulates an external drag-and-drop of files and/or data onto an element
/// (e.g. an upload drop zone), dispatching `dragenter`/`dragover`/`drop` with a
/// synthetic `DataTransfer`. This is distinct from
/// [`Locator::drag_to()`](crate::protocol::Locator::drag_to), which drags one
/// element onto another within the page.
///
/// Set `files` and/or `data` (the driver requires at least one).
///
/// # Example
///
/// ```no_run
/// use playwright_rs::{DropOptions, FilePayload, Position};
///
/// // Drop an in-memory file onto a drop zone
/// let file = FilePayload::new("note.txt", "text/plain", b"hello".to_vec());
/// let options = DropOptions::builder().file(file).build();
///
/// // Drop MIME-typed data (e.g. a dragged URL) at a specific point
/// let options = DropOptions::builder()
/// .data("text/uri-list", "https://example.com")
/// .position(Position { x: 10.0, y: 10.0 })
/// .build();
/// ```
///
/// See: <https://playwright.dev/docs/api/class-locator#locator-drop>
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct DropOptions {
/// In-memory files to drop (serialized to the protocol's `payloads`).
pub files: Vec<FilePayload>,
/// MIME-typed data entries to drop, as `(mime_type, value)` pairs.
pub data: Vec<(String, String)>,
/// Point within the element to drop at (relative to its top-left corner).
pub position: Option<Position>,
/// Maximum time in milliseconds.
pub timeout: Option<f64>,
}
impl DropOptions {
/// Create a new builder for DropOptions
pub fn builder() -> DropOptionsBuilder {
DropOptionsBuilder::default()
}
/// Convert options to JSON value for protocol
pub(crate) fn to_json(&self) -> serde_json::Value {
use base64::{Engine as _, engine::general_purpose};
let mut json = serde_json::json!({});
if !self.files.is_empty() {
let payloads: Vec<serde_json::Value> = self
.files
.iter()
.map(|f| {
serde_json::json!({
"name": f.name,
"mimeType": f.mime_type,
"buffer": general_purpose::STANDARD.encode(&f.buffer),
})
})
.collect();
json["payloads"] = serde_json::Value::Array(payloads);
}
if !self.data.is_empty() {
let data: Vec<serde_json::Value> = self
.data
.iter()
.map(|(mime, value)| serde_json::json!({ "mimeType": mime, "value": value }))
.collect();
json["data"] = serde_json::Value::Array(data);
}
if let Some(position) = &self.position {
json["position"] =
serde_json::to_value(position).expect("serialization of position cannot fail");
}
// Timeout is required in Playwright 1.56.1+
if let Some(timeout) = self.timeout {
json["timeout"] = serde_json::json!(timeout);
} else {
json["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
}
json
}
}
/// Builder for DropOptions
///
/// Provides a fluent API for constructing drop options.
#[derive(Debug, Clone, Default)]
pub struct DropOptionsBuilder {
files: Vec<FilePayload>,
data: Vec<(String, String)>,
position: Option<Position>,
timeout: Option<f64>,
}
impl DropOptionsBuilder {
/// Add one in-memory file to drop.
pub fn file(mut self, file: FilePayload) -> Self {
self.files.push(file);
self
}
/// Set the in-memory files to drop (replaces any already added).
pub fn files(mut self, files: Vec<FilePayload>) -> Self {
self.files = files;
self
}
/// Add a MIME-typed data entry to drop (e.g. `"text/plain"` or `"text/uri-list"`).
pub fn data(mut self, mime_type: impl Into<String>, value: impl Into<String>) -> Self {
self.data.push((mime_type.into(), value.into()));
self
}
/// Set the point within the element to drop at (relative to top-left corner).
pub fn position(mut self, position: Position) -> Self {
self.position = Some(position);
self
}
/// Set timeout in milliseconds.
pub fn timeout(mut self, timeout: f64) -> Self {
self.timeout = Some(timeout);
self
}
/// Build the DropOptions
pub fn build(self) -> DropOptions {
DropOptions {
files: self.files,
data: self.data,
position: self.position,
timeout: self.timeout,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_drop_options_default() {
let json = DropOptions::builder().build().to_json();
assert!(json["timeout"].is_number());
assert!(json.get("payloads").is_none());
assert!(json.get("data").is_none());
assert!(json.get("position").is_none());
}
#[test]
fn test_drop_options_file_payload() {
let file = FilePayload::new("note.txt", "text/plain", b"hi".to_vec());
let json = DropOptions::builder().file(file).build().to_json();
assert_eq!(json["payloads"][0]["name"], "note.txt");
assert_eq!(json["payloads"][0]["mimeType"], "text/plain");
assert_eq!(json["payloads"][0]["buffer"], "aGk="); // base64("hi")
}
#[test]
fn test_drop_options_data() {
let json = DropOptions::builder()
.data("text/uri-list", "https://example.com")
.build()
.to_json();
assert_eq!(json["data"][0]["mimeType"], "text/uri-list");
assert_eq!(json["data"][0]["value"], "https://example.com");
}
#[test]
fn test_drop_options_position_and_timeout() {
let json = DropOptions::builder()
.position(Position { x: 10.0, y: 20.0 })
.timeout(5000.0)
.build()
.to_json();
assert_eq!(json["position"]["x"], 10.0);
assert_eq!(json["position"]["y"], 20.0);
assert_eq!(json["timeout"], 5000.0);
}
}