StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use super::*; |
| 2 | use crate::clipboard::{ClipboardContent, ImageData}; |
| 3 | |
| 4 | // ============================================================================ |
| 5 | // HELPER FUNCTIONS (shared across tests) |
| 6 | // ============================================================================ |
| 7 | |
| 8 | #[cfg(any(target_os = "linux", target_os = "windows"))] |
| 9 | fn create_rgba_data(w: usize, h: usize) -> Vec<u8> { |
| 10 | // Simple test pattern: red gradient |
| 11 | (0..h) |
| 12 | .flat_map(|y| { |
| 13 | (0..w).flat_map(move |x| [((x * 255) / w) as u8, ((y * 255) / h) as u8, 128, 255]) |
| 14 | }) |
| 15 | .collect() |
| 16 | } |
| 17 | |
| 18 | #[cfg(any(target_os = "linux", target_os = "windows"))] |
| 19 | fn create_simple_png() -> Vec<u8> { |
| 20 | // PNG header for 1x1 red pixel |
| 21 | vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] // PNG signature |
| 22 | } |
| 23 | |
| 24 | #[cfg(any(target_os = "linux", target_os = "windows"))] |
| 25 | fn create_simple_jpeg() -> Vec<u8> { |
| 26 | // JPEG header |
| 27 | vec![0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46] |
| 28 | } |
| 29 | |
| 30 | #[cfg(any(target_os = "linux", target_os = "windows"))] |
| 31 | fn create_simple_gif() -> Vec<u8> { |
| 32 | // GIF header |
| 33 | let mut data = Vec::new(); |
| 34 | data.extend_from_slice(b"GIF87a"); |
| 35 | data.extend_from_slice(&[1, 0, 1, 0, 0, 0, 0]); // minimal 1x1 GIF |
| 36 | data |
| 37 | } |
| 38 | |
| 39 | #[cfg(any(target_os = "linux", target_os = "windows"))] |
| 40 | fn create_simple_webp() -> Vec<u8> { |
| 41 | // WebP header |
| 42 | let mut data = Vec::new(); |
| 43 | data.extend_from_slice(b"RIFF"); |
| 44 | data.extend_from_slice(&[12, 0, 0, 0]); // file size |
| 45 | data.extend_from_slice(b"WEBP"); |
| 46 | data.extend_from_slice(b"VP8 "); |
| 47 | data |
| 48 | } |
| 49 | |
| 50 | #[cfg(any(target_os = "linux", target_os = "windows"))] |
| 51 | fn assert_valid_png(result: Option<ImageData>) { |
| 52 | let image_data = result.expect("Should process image successfully"); |
| 53 | assert_eq!(image_data.mime_type, "image/png"); |
| 54 | assert_eq!(&image_data.data[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]); // PNG header |
| 55 | } |
| 56 | |
| 57 | // ============================================================================ |
| 58 | // FILENAME EXTRACTION TESTS |
| 59 | // ============================================================================ |
| 60 | |
| 61 | #[test] |
| 62 | fn test_extract_filename_from_html() { |
| 63 | // Test extraction from src attribute with file:// URL (common on macOS) |
| 64 | let html1 = r##"<img src="file:///Users/test/Pictures/screenshot.png" alt="Screenshot">"##; |
| 65 | let filename = extract_filename_from_html(html1); |
| 66 | assert_eq!(filename, Some("screenshot.png".to_string())); |
| 67 | |
| 68 | // Test extraction from src attribute with http URL |
| 69 | let html2 = r##"<img src="https://example.com/images/photo.jpg" alt="Photo">"##; |
| 70 | let filename = extract_filename_from_html(html2); |
| 71 | assert_eq!(filename, Some("photo.jpg".to_string())); |
| 72 | |
| 73 | // Test extraction from title attribute |
| 74 | let html3 = r##"<img title="document.gif" src="data:image/gif;base64,R0lGOD...">"##; |
| 75 | let filename = extract_filename_from_html(html3); |
| 76 | assert_eq!(filename, Some("document.gif".to_string())); |
| 77 | |
| 78 | // Test extraction from alt attribute |
| 79 | let html4 = r##"<img alt="image.webp" src="data:image/webp;base64,UklGR...">"##; |
| 80 | let filename = extract_filename_from_html(html4); |
| 81 | assert_eq!(filename, Some("image.webp".to_string())); |
| 82 | |
| 83 | // Test extraction from free text |
| 84 | let html5 = r##"<div>Here is my image: myfile.jpeg that I copied</div>"##; |
| 85 | let filename = extract_filename_from_html(html5); |
| 86 | assert_eq!(filename, Some("myfile.jpeg".to_string())); |
| 87 | |
| 88 | // Test no filename found |
| 89 | let html6 = r##"<div>Just some text with no image references</div>"##; |
| 90 | let filename = extract_filename_from_html(html6); |
| 91 | assert_eq!(filename, None); |
| 92 | |
| 93 | // Test non-image extension ignored |
| 94 | let html7 = r##"<div>document.pdf and archive.zip should be ignored</div>"##; |
| 95 | let filename = extract_filename_from_html(html7); |
| 96 | assert_eq!(filename, None); |
| 97 | |
| 98 | // Test complex path extraction with Windows-style paths |
| 99 | let html8 = |
| 100 | r##"<img src="file://C:\Users\John%20Doe\Desktop\My%20Images\vacation-photo.png">"##; |
| 101 | let filename = extract_filename_from_html(html8); |
| 102 | assert_eq!(filename, Some("vacation-photo.png".to_string())); |
| 103 | |
| 104 | // Test case-insensitive extension matching |
| 105 | let html9 = r##"<img src="test.PNG" alt="Test">"##; |
| 106 | let filename = extract_filename_from_html(html9); |
| 107 | assert_eq!(filename, Some("test.PNG".to_string())); |
| 108 | |
| 109 | // Test extraction with various punctuation |
| 110 | let html10 = r##"<div>Look at "my-image.jpg", (another.gif), or <file.webp>!</div>"##; |
| 111 | let filename = extract_filename_from_html(html10); |
| 112 | // Should find the first one |
| 113 | assert_eq!(filename, Some("my-image.jpg".to_string())); |
| 114 | } |
| 115 | |
| 116 | #[test] |
| 117 | fn test_extract_filename_from_text() { |
| 118 | // Test full file path |
| 119 | let file_path = "/Users/test/Documents/screenshot.png"; |
| 120 | let result = extract_filename_from_text(file_path); |
| 121 | assert_eq!(result, Some("screenshot.png".to_string())); |
| 122 | |
| 123 | // Test Windows path |
| 124 | let windows_path = "C:\\Users\\test\\Documents\\image.jpg"; |
| 125 | let result = extract_filename_from_text(windows_path); |
| 126 | assert_eq!(result, Some("image.jpg".to_string())); |
| 127 | |
| 128 | // Test file:// URL |
| 129 | let file_url = "file:///Users/test/screenshot.gif"; |
| 130 | let result = extract_filename_from_text(file_url); |
| 131 | assert_eq!(result, Some("screenshot.gif".to_string())); |
| 132 | |
| 133 | // Test multiline with file path |
| 134 | let multiline = "Some text\n/path/to/image.webp\nMore text"; |
| 135 | let result = extract_filename_from_text(multiline); |
| 136 | assert_eq!(result, Some("image.webp".to_string())); |
| 137 | |
| 138 | // Test non-image file (should return None) |
| 139 | let text_file = "/Users/test/document.txt"; |
| 140 | let result = extract_filename_from_text(text_file); |
| 141 | assert_eq!(result, None); |
| 142 | |
| 143 | // Test no file path |
| 144 | let plain_text = "Just some plain text"; |
| 145 | let result = extract_filename_from_text(plain_text); |
| 146 | assert_eq!(result, None); |
| 147 | |
| 148 | // Test just filename |
| 149 | let just_filename = "my-screenshot.png"; |
| 150 | let result = extract_filename_from_text(just_filename); |
| 151 | assert_eq!(result, Some("my-screenshot.png".to_string())); |
| 152 | |
| 153 | // Test empty string |
| 154 | let empty = ""; |
| 155 | let result = extract_filename_from_text(empty); |
| 156 | assert_eq!(result, None); |
| 157 | } |
| 158 | |
| 159 | #[test] |
| 160 | fn test_extract_filename_from_clipboard_content() { |
| 161 | // Test HTML takes precedence over text |
| 162 | let html_content = Some(r##"<img src="test.png" alt="Test">"##.to_string()); |
| 163 | let text_content = "other-file.jpg"; |
| 164 | let result = extract_filename_from_clipboard_content(&html_content, text_content); |
| 165 | assert_eq!(result, Some("test.png".to_string())); |
| 166 | |
| 167 | // Test fallback to text when HTML has no filename |
| 168 | let html_content = Some("<div>No images here</div>".to_string()); |
| 169 | let text_content = "/path/to/image.gif"; |
| 170 | let result = extract_filename_from_clipboard_content(&html_content, text_content); |
| 171 | assert_eq!(result, Some("image.gif".to_string())); |
| 172 | |
| 173 | // Test fallback to text when no HTML |
| 174 | let html_content = None; |
| 175 | let text_content = "screenshot.webp"; |
| 176 | let result = extract_filename_from_clipboard_content(&html_content, text_content); |
| 177 | assert_eq!(result, Some("screenshot.webp".to_string())); |
| 178 | |
| 179 | // Test no filename found |
| 180 | let html_content = Some("<div>Just text</div>".to_string()); |
| 181 | let text_content = "No images here either"; |
| 182 | let result = extract_filename_from_clipboard_content(&html_content, text_content); |
| 183 | assert_eq!(result, None); |
| 184 | } |
| 185 | |
| 186 | // ============================================================================ |
| 187 | // IMAGE PROCESSING TESTS (Linux/Windows platforms only) |
| 188 | // ============================================================================ |
| 189 | |
| 190 | #[cfg(any(target_os = "linux", target_os = "windows"))] |
| 191 | mod image_processing_tests { |
| 192 | use super::*; |
| 193 | |
| 194 | #[test] |
| 195 | fn test_rgba_bitmap_processing() { |
| 196 | let arboard_image = arboard::ImageData { |
| 197 | width: 8, |
| 198 | height: 6, |
| 199 | bytes: create_rgba_data(8, 6).into(), |
| 200 | }; |
| 201 | assert_valid_png(process_clipboard_image(&arboard_image, None)); |
| 202 | } |
| 203 | |
| 204 | #[test] |
| 205 | fn test_invalid_data_rejection() { |
| 206 | let arboard_image = arboard::ImageData { |
| 207 | width: 10, |
| 208 | height: 10, |
| 209 | bytes: vec![1, 2, 3, 4, 5].into(), |
| 210 | }; |
| 211 | assert!(process_clipboard_image(&arboard_image, None).is_none()); |
| 212 | } |
| 213 | |
| 214 | #[test] |
| 215 | fn test_various_dimensions() { |
| 216 | for (w, h) in [(100, 100), (782, 297), (1, 1)] { |
| 217 | let arboard_image = arboard::ImageData { |
| 218 | width: w, |
| 219 | height: h, |
| 220 | bytes: create_rgba_data(w, h).into(), |
| 221 | }; |
| 222 | let result = process_clipboard_image(&arboard_image, None) |
| 223 | .unwrap_or_else(|| panic!("Failed to process {w}x{h} image")); |
| 224 | let loaded = image::load_from_memory(&result.data) |
| 225 | .unwrap_or_else(|e| panic!("Failed to load processed {w}x{h} image: {e}")); |
| 226 | assert_eq!((loaded.width(), loaded.height()), (w as u32, h as u32)); |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | #[test] |
| 231 | fn test_format_preservation_and_detection() { |
| 232 | let test_cases = vec![ |
| 233 | (create_simple_png(), "image/png", "test.png"), |
| 234 | (create_simple_jpeg(), "image/jpeg", "test.jpg"), |
| 235 | (create_simple_gif(), "image/gif", "test.gif"), |
| 236 | (create_simple_webp(), "image/webp", "test.webp"), |
| 237 | ]; |
| 238 | |
| 239 | for (data, expected_mime, filename) in test_cases { |
| 240 | let result = try_preserve_original_format(&data, Some(filename.to_string())); |
| 241 | if let Some(image_data) = result { |
| 242 | assert_eq!(image_data.mime_type, expected_mime); |
| 243 | assert_eq!(image_data.filename, Some(filename.to_string())); |
| 244 | // Format preservation should keep original data |
| 245 | assert_eq!(image_data.data, data); |
| 246 | } |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | #[test] |
| 251 | fn test_unsupported_format_fallback() { |
| 252 | // Create some random data that doesn't match any supported format |
| 253 | let unsupported_data = vec![0x50, 0x4B, 0x03, 0x04]; // ZIP signature |
| 254 | let arboard_image = arboard::ImageData { |
| 255 | width: 4, |
| 256 | height: 4, |
| 257 | bytes: unsupported_data.into(), |
| 258 | }; |
| 259 | |
| 260 | // Should return None since ZIP is not a supported image format |
| 261 | let result = process_clipboard_image(&arboard_image, None); |
| 262 | assert!(result.is_none(), "Should reject unsupported format"); |
| 263 | } |
| 264 | |
| 265 | #[test] |
| 266 | fn test_convert_raw_bitmap_to_png() { |
| 267 | // Test valid conversion |
| 268 | let width = 2; |
| 269 | let height = 2; |
| 270 | let rgba_data = vec![ |
| 271 | 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255, |
| 272 | ]; |
| 273 | |
| 274 | let result = |
| 275 | convert_raw_bitmap_to_png(width, height, rgba_data, Some("test.png".to_string())); |
| 276 | if let Some(image_data) = result { |
| 277 | assert_eq!(image_data.mime_type, "image/png"); |
| 278 | assert_eq!(image_data.filename, Some("test.png".to_string())); |
| 279 | assert!(!image_data.data.is_empty()); |
| 280 | } |
| 281 | |
| 282 | // Test invalid dimensions |
| 283 | let result = convert_raw_bitmap_to_png(usize::MAX, 1, vec![255, 0, 0, 255], None); |
| 284 | assert!(result.is_none()); |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | // ============================================================================ |
| 289 | // CLIPBOARD CONTENT STRUCTURE TESTS |
| 290 | // ============================================================================ |
| 291 | |
| 292 | #[test] |
| 293 | fn test_clipboard_content_with_images() { |
| 294 | let image_data = ImageData { |
| 295 | data: vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], |
| 296 | mime_type: "image/png".to_string(), |
| 297 | filename: Some("test.png".to_string()), |
| 298 | }; |
| 299 | |
| 300 | let content = ClipboardContent { |
| 301 | plain_text: "Test text".to_string(), |
| 302 | html: Some(r##"<img src="test.png">"##.to_string()), |
| 303 | images: Some(vec![image_data.clone()]), |
| 304 | paths: None, |
| 305 | }; |
| 306 | |
| 307 | assert!(!content.is_empty()); |
| 308 | assert!(content.images.is_some()); |
| 309 | assert_eq!(content.images.as_ref().unwrap().len(), 1); |
| 310 | assert_eq!(content.images.as_ref().unwrap()[0].mime_type, "image/png"); |
| 311 | } |
| 312 |