we (web engine): Experimental web browser project to understand the limits of Claude
1//! Resource loader: fetch URLs and decode their content.
2//!
3//! Brings together `net` (HTTP client), `encoding` (charset detection and decoding),
4//! `url` (URL parsing and resolution), and `image` (image decoding) into a single
5//! `ResourceLoader` that the browser uses to load web pages and subresources.
6
7use std::fmt;
8
9use we_encoding::sniff::sniff_encoding;
10use we_encoding::Encoding;
11use we_net::client::{ClientError, HttpClient};
12use we_net::http::ContentType;
13use we_url::data_url::{is_data_url, parse_data_url};
14use we_url::Url;
15
16// ---------------------------------------------------------------------------
17// Error type
18// ---------------------------------------------------------------------------
19
20/// Errors that can occur during resource loading.
21#[derive(Debug)]
22pub enum LoadError {
23 /// URL parsing failed.
24 InvalidUrl(String),
25 /// Network or HTTP error from the underlying client.
26 Network(ClientError),
27 /// HTTP response indicated an error status.
28 HttpStatus { status: u16, reason: String },
29 /// Encoding or decoding error.
30 Encoding(String),
31}
32
33impl fmt::Display for LoadError {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Self::InvalidUrl(s) => write!(f, "invalid URL: {s}"),
37 Self::Network(e) => write!(f, "network error: {e}"),
38 Self::HttpStatus { status, reason } => {
39 write!(f, "HTTP {status} {reason}")
40 }
41 Self::Encoding(s) => write!(f, "encoding error: {s}"),
42 }
43 }
44}
45
46impl From<ClientError> for LoadError {
47 fn from(e: ClientError) -> Self {
48 Self::Network(e)
49 }
50}
51
52// ---------------------------------------------------------------------------
53// Resource types
54// ---------------------------------------------------------------------------
55
56/// A loaded resource with its decoded content and metadata.
57#[derive(Debug)]
58pub enum Resource {
59 /// An HTML document.
60 Html {
61 text: String,
62 base_url: Url,
63 encoding: Encoding,
64 },
65 /// A CSS stylesheet.
66 Css { text: String, url: Url },
67 /// A decoded image.
68 Image {
69 data: Vec<u8>,
70 mime_type: String,
71 url: Url,
72 },
73 /// Any other resource type (binary).
74 Other {
75 data: Vec<u8>,
76 mime_type: String,
77 url: Url,
78 },
79}
80
81// ---------------------------------------------------------------------------
82// ResourceLoader
83// ---------------------------------------------------------------------------
84
85/// Loads resources over HTTP/HTTPS with encoding detection and content-type handling.
86pub struct ResourceLoader {
87 client: HttpClient,
88}
89
90impl ResourceLoader {
91 /// Create a new resource loader with default settings.
92 pub fn new() -> Self {
93 Self {
94 client: HttpClient::new(),
95 }
96 }
97
98 /// Fetch a resource at the given URL.
99 ///
100 /// Determines the resource type from the HTTP Content-Type header, decodes
101 /// text resources using the appropriate character encoding (per WHATWG spec),
102 /// and returns the result as a typed `Resource`.
103 ///
104 /// Handles `data:` and `about:` URLs locally without network access.
105 pub fn fetch(&mut self, url: &Url) -> Result<Resource, LoadError> {
106 // Handle data: URLs without network fetch.
107 if url.scheme() == "data" {
108 return fetch_data_url(&url.serialize());
109 }
110
111 // Handle about: URLs without network fetch.
112 if url.scheme() == "about" {
113 return fetch_about_url(url);
114 }
115
116 let response = self.client.get(url)?;
117
118 // Check for HTTP error status codes
119 if response.status_code >= 400 {
120 return Err(LoadError::HttpStatus {
121 status: response.status_code,
122 reason: response.reason.clone(),
123 });
124 }
125
126 let content_type = response.content_type();
127 let mime = content_type
128 .as_ref()
129 .map(|ct| ct.mime_type.as_str())
130 .unwrap_or("application/octet-stream");
131
132 match classify_mime(mime) {
133 MimeClass::Html => {
134 let (text, encoding) =
135 decode_text_resource(&response.body, content_type.as_ref(), true);
136 Ok(Resource::Html {
137 text,
138 base_url: url.clone(),
139 encoding,
140 })
141 }
142 MimeClass::Css => {
143 let (text, _encoding) =
144 decode_text_resource(&response.body, content_type.as_ref(), false);
145 Ok(Resource::Css {
146 text,
147 url: url.clone(),
148 })
149 }
150 MimeClass::Image => Ok(Resource::Image {
151 data: response.body,
152 mime_type: mime.to_string(),
153 url: url.clone(),
154 }),
155 MimeClass::Other => {
156 // Check if it's a text type we should decode
157 if mime.starts_with("text/") {
158 let (text, _encoding) =
159 decode_text_resource(&response.body, content_type.as_ref(), false);
160 Ok(Resource::Other {
161 data: text.into_bytes(),
162 mime_type: mime.to_string(),
163 url: url.clone(),
164 })
165 } else {
166 Ok(Resource::Other {
167 data: response.body,
168 mime_type: mime.to_string(),
169 url: url.clone(),
170 })
171 }
172 }
173 }
174 }
175
176 /// Fetch a URL string, resolving it against an optional base URL.
177 ///
178 /// Handles `data:` and `about:` URLs locally without network access.
179 pub fn fetch_url(&mut self, url_str: &str, base: Option<&Url>) -> Result<Resource, LoadError> {
180 // Handle data URLs directly — no network fetch needed.
181 if is_data_url(url_str) {
182 return fetch_data_url(url_str);
183 }
184
185 // Handle about: URLs without network fetch.
186 if url_str.starts_with("about:") {
187 let url =
188 Url::parse(url_str).map_err(|_| LoadError::InvalidUrl(url_str.to_string()))?;
189 return fetch_about_url(&url);
190 }
191
192 let url = match base {
193 Some(base_url) => Url::parse_with_base(url_str, base_url)
194 .or_else(|_| Url::parse(url_str))
195 .map_err(|_| LoadError::InvalidUrl(url_str.to_string()))?,
196 None => Url::parse(url_str).map_err(|_| LoadError::InvalidUrl(url_str.to_string()))?,
197 };
198 self.fetch(&url)
199 }
200}
201
202impl Default for ResourceLoader {
203 fn default() -> Self {
204 Self::new()
205 }
206}
207
208// ---------------------------------------------------------------------------
209// MIME classification
210// ---------------------------------------------------------------------------
211
212enum MimeClass {
213 Html,
214 Css,
215 Image,
216 Other,
217}
218
219fn classify_mime(mime: &str) -> MimeClass {
220 match mime {
221 "text/html" | "application/xhtml+xml" => MimeClass::Html,
222 "text/css" => MimeClass::Css,
223 "image/png" | "image/jpeg" | "image/gif" | "image/webp" | "image/svg+xml" => {
224 MimeClass::Image
225 }
226 _ => MimeClass::Other,
227 }
228}
229
230// ---------------------------------------------------------------------------
231// Text decoding
232// ---------------------------------------------------------------------------
233
234/// Decode a text resource's bytes to a String using WHATWG encoding sniffing.
235///
236/// For HTML resources, uses BOM > HTTP charset > meta prescan > default.
237/// For non-HTML text resources, uses BOM > HTTP charset > default (UTF-8).
238fn decode_text_resource(
239 bytes: &[u8],
240 content_type: Option<&ContentType>,
241 is_html: bool,
242) -> (String, Encoding) {
243 let http_ct_value = content_type.map(|ct| {
244 // Reconstruct a Content-Type header value for the sniffing function
245 match &ct.charset {
246 Some(charset) => format!("{}; charset={}", ct.mime_type, charset),
247 None => ct.mime_type.clone(),
248 }
249 });
250
251 if is_html {
252 // Full WHATWG sniffing: BOM > HTTP > meta prescan > default (Windows-1252)
253 let (encoding, _source) = sniff_encoding(bytes, http_ct_value.as_deref());
254 let text = decode_with_bom_handling(bytes, encoding);
255 (text, encoding)
256 } else {
257 // Non-HTML: BOM > HTTP charset > default (UTF-8)
258 let (bom_enc, after_bom) = we_encoding::bom_sniff(bytes);
259 if let Some(enc) = bom_enc {
260 let text = we_encoding::decode(after_bom, enc);
261 return (text, enc);
262 }
263
264 // Try HTTP charset
265 if let Some(charset) = content_type.and_then(|ct| ct.charset.as_deref()) {
266 if let Some(enc) = we_encoding::lookup(charset) {
267 let text = we_encoding::decode(bytes, enc);
268 return (text, enc);
269 }
270 }
271
272 // Default to UTF-8 for non-HTML text
273 let text = we_encoding::decode(bytes, Encoding::Utf8);
274 (text, Encoding::Utf8)
275 }
276}
277
278/// Decode bytes with BOM handling — strip BOM bytes before decoding.
279fn decode_with_bom_handling(bytes: &[u8], encoding: Encoding) -> String {
280 let (bom_enc, after_bom) = we_encoding::bom_sniff(bytes);
281 if bom_enc.is_some() {
282 // BOM was present — decode the bytes after the BOM
283 we_encoding::decode(after_bom, encoding)
284 } else {
285 we_encoding::decode(bytes, encoding)
286 }
287}
288
289// ---------------------------------------------------------------------------
290// Data URL handling
291// ---------------------------------------------------------------------------
292
293/// Fetch a data URL, decoding its payload and returning the appropriate Resource type.
294fn fetch_data_url(url_str: &str) -> Result<Resource, LoadError> {
295 let parsed = parse_data_url(url_str)
296 .map_err(|e| LoadError::InvalidUrl(format!("data URL error: {e}")))?;
297
298 let mime = &parsed.mime_type;
299
300 // Create a synthetic Url for the resource metadata.
301 let url = Url::parse(url_str).map_err(|_| LoadError::InvalidUrl(url_str.to_string()))?;
302
303 match classify_mime(mime) {
304 MimeClass::Html => {
305 let encoding = charset_to_encoding(parsed.charset.as_deref());
306 let text = we_encoding::decode(&parsed.data, encoding);
307 Ok(Resource::Html {
308 text,
309 base_url: url,
310 encoding,
311 })
312 }
313 MimeClass::Css => {
314 let encoding = charset_to_encoding(parsed.charset.as_deref());
315 let text = we_encoding::decode(&parsed.data, encoding);
316 Ok(Resource::Css { text, url })
317 }
318 MimeClass::Image => Ok(Resource::Image {
319 data: parsed.data,
320 mime_type: mime.to_string(),
321 url,
322 }),
323 MimeClass::Other => {
324 if mime.starts_with("text/") {
325 let encoding = charset_to_encoding(parsed.charset.as_deref());
326 let text = we_encoding::decode(&parsed.data, encoding);
327 Ok(Resource::Other {
328 data: text.into_bytes(),
329 mime_type: mime.to_string(),
330 url,
331 })
332 } else {
333 Ok(Resource::Other {
334 data: parsed.data,
335 mime_type: mime.to_string(),
336 url,
337 })
338 }
339 }
340 }
341}
342
343// ---------------------------------------------------------------------------
344// about: URL handling
345// ---------------------------------------------------------------------------
346
347/// The minimal HTML document for about:blank.
348pub const ABOUT_BLANK_HTML: &str = "<!DOCTYPE html><html><head></head><body></body></html>";
349
350/// Fetch an about: URL, returning the appropriate resource.
351///
352/// Currently only `about:blank` is supported, which returns an empty HTML
353/// document with UTF-8 encoding.
354fn fetch_about_url(url: &Url) -> Result<Resource, LoadError> {
355 match url.path().as_str() {
356 "blank" => Ok(Resource::Html {
357 text: ABOUT_BLANK_HTML.to_string(),
358 base_url: url.clone(),
359 encoding: Encoding::Utf8,
360 }),
361 other => Err(LoadError::InvalidUrl(format!(
362 "unsupported about: URL: about:{other}"
363 ))),
364 }
365}
366
367/// Map a charset name to an Encoding, defaulting to UTF-8.
368fn charset_to_encoding(charset: Option<&str>) -> Encoding {
369 charset
370 .and_then(we_encoding::lookup)
371 .unwrap_or(Encoding::Utf8)
372}
373
374// ---------------------------------------------------------------------------
375// Tests
376// ---------------------------------------------------------------------------
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 // -----------------------------------------------------------------------
383 // LoadError Display
384 // -----------------------------------------------------------------------
385
386 #[test]
387 fn load_error_display_invalid_url() {
388 let e = LoadError::InvalidUrl("bad://url".to_string());
389 assert_eq!(e.to_string(), "invalid URL: bad://url");
390 }
391
392 #[test]
393 fn load_error_display_http_status() {
394 let e = LoadError::HttpStatus {
395 status: 404,
396 reason: "Not Found".to_string(),
397 };
398 assert_eq!(e.to_string(), "HTTP 404 Not Found");
399 }
400
401 #[test]
402 fn load_error_display_encoding() {
403 let e = LoadError::Encoding("bad charset".to_string());
404 assert_eq!(e.to_string(), "encoding error: bad charset");
405 }
406
407 // -----------------------------------------------------------------------
408 // MIME classification
409 // -----------------------------------------------------------------------
410
411 #[test]
412 fn classify_text_html() {
413 assert!(matches!(classify_mime("text/html"), MimeClass::Html));
414 }
415
416 #[test]
417 fn classify_xhtml() {
418 assert!(matches!(
419 classify_mime("application/xhtml+xml"),
420 MimeClass::Html
421 ));
422 }
423
424 #[test]
425 fn classify_text_css() {
426 assert!(matches!(classify_mime("text/css"), MimeClass::Css));
427 }
428
429 #[test]
430 fn classify_image_png() {
431 assert!(matches!(classify_mime("image/png"), MimeClass::Image));
432 }
433
434 #[test]
435 fn classify_image_jpeg() {
436 assert!(matches!(classify_mime("image/jpeg"), MimeClass::Image));
437 }
438
439 #[test]
440 fn classify_image_gif() {
441 assert!(matches!(classify_mime("image/gif"), MimeClass::Image));
442 }
443
444 #[test]
445 fn classify_application_json() {
446 assert!(matches!(
447 classify_mime("application/json"),
448 MimeClass::Other
449 ));
450 }
451
452 #[test]
453 fn classify_text_plain() {
454 assert!(matches!(classify_mime("text/plain"), MimeClass::Other));
455 }
456
457 #[test]
458 fn classify_octet_stream() {
459 assert!(matches!(
460 classify_mime("application/octet-stream"),
461 MimeClass::Other
462 ));
463 }
464
465 // -----------------------------------------------------------------------
466 // Text decoding — HTML
467 // -----------------------------------------------------------------------
468
469 #[test]
470 fn decode_html_utf8_bom() {
471 let bytes = b"\xEF\xBB\xBF<html>Hello</html>";
472 let (text, enc) = decode_text_resource(bytes, None, true);
473 assert_eq!(enc, Encoding::Utf8);
474 assert_eq!(text, "<html>Hello</html>");
475 }
476
477 #[test]
478 fn decode_html_utf8_from_http_charset() {
479 let ct = ContentType {
480 mime_type: "text/html".to_string(),
481 charset: Some("utf-8".to_string()),
482 };
483 let bytes = b"<html>Hello</html>";
484 let (text, enc) = decode_text_resource(bytes, Some(&ct), true);
485 assert_eq!(enc, Encoding::Utf8);
486 assert_eq!(text, "<html>Hello</html>");
487 }
488
489 #[test]
490 fn decode_html_meta_charset() {
491 let html = b"<meta charset=\"utf-8\"><html>Hello</html>";
492 let (text, enc) = decode_text_resource(html, None, true);
493 assert_eq!(enc, Encoding::Utf8);
494 assert!(text.contains("Hello"));
495 }
496
497 #[test]
498 fn decode_html_default_windows_1252() {
499 let bytes = b"<html>Hello</html>";
500 let (text, enc) = decode_text_resource(bytes, None, true);
501 assert_eq!(enc, Encoding::Windows1252);
502 assert!(text.contains("Hello"));
503 }
504
505 #[test]
506 fn decode_html_windows_1252_special_chars() {
507 // \x93 and \x94 are left/right double quotation marks in Windows-1252
508 let bytes = b"<html>\x93Hello\x94</html>";
509 let (text, enc) = decode_text_resource(bytes, None, true);
510 assert_eq!(enc, Encoding::Windows1252);
511 assert!(text.contains('\u{201C}')); // left double quote
512 assert!(text.contains('\u{201D}')); // right double quote
513 }
514
515 #[test]
516 fn decode_html_bom_beats_http_charset() {
517 let ct = ContentType {
518 mime_type: "text/html".to_string(),
519 charset: Some("windows-1252".to_string()),
520 };
521 let mut bytes = vec![0xEF, 0xBB, 0xBF];
522 bytes.extend_from_slice(b"<html>Hello</html>");
523 let (text, enc) = decode_text_resource(&bytes, Some(&ct), true);
524 assert_eq!(enc, Encoding::Utf8);
525 assert_eq!(text, "<html>Hello</html>");
526 }
527
528 // -----------------------------------------------------------------------
529 // Text decoding — non-HTML (CSS, etc.)
530 // -----------------------------------------------------------------------
531
532 #[test]
533 fn decode_css_utf8_default() {
534 let bytes = b"body { color: red; }";
535 let (text, enc) = decode_text_resource(bytes, None, false);
536 assert_eq!(enc, Encoding::Utf8);
537 assert_eq!(text, "body { color: red; }");
538 }
539
540 #[test]
541 fn decode_css_bom_utf8() {
542 let bytes = b"\xEF\xBB\xBFbody { color: red; }";
543 let (text, enc) = decode_text_resource(bytes, None, false);
544 assert_eq!(enc, Encoding::Utf8);
545 assert_eq!(text, "body { color: red; }");
546 }
547
548 #[test]
549 fn decode_css_http_charset() {
550 let ct = ContentType {
551 mime_type: "text/css".to_string(),
552 charset: Some("utf-8".to_string()),
553 };
554 let bytes = b"body { color: red; }";
555 let (text, enc) = decode_text_resource(bytes, Some(&ct), false);
556 assert_eq!(enc, Encoding::Utf8);
557 assert_eq!(text, "body { color: red; }");
558 }
559
560 // -----------------------------------------------------------------------
561 // BOM handling
562 // -----------------------------------------------------------------------
563
564 #[test]
565 fn decode_with_bom_strips_utf8_bom() {
566 let bytes = b"\xEF\xBB\xBFHello";
567 let text = decode_with_bom_handling(bytes, Encoding::Utf8);
568 assert_eq!(text, "Hello");
569 }
570
571 #[test]
572 fn decode_without_bom_passes_through() {
573 let bytes = b"Hello";
574 let text = decode_with_bom_handling(bytes, Encoding::Utf8);
575 assert_eq!(text, "Hello");
576 }
577
578 #[test]
579 fn decode_with_utf16le_bom() {
580 let bytes = b"\xFF\xFEH\x00e\x00l\x00l\x00o\x00";
581 let text = decode_with_bom_handling(bytes, Encoding::Utf16Le);
582 assert_eq!(text, "Hello");
583 }
584
585 #[test]
586 fn decode_with_utf16be_bom() {
587 let bytes = b"\xFE\xFF\x00H\x00e\x00l\x00l\x00o";
588 let text = decode_with_bom_handling(bytes, Encoding::Utf16Be);
589 assert_eq!(text, "Hello");
590 }
591
592 // -----------------------------------------------------------------------
593 // ResourceLoader construction
594 // -----------------------------------------------------------------------
595
596 #[test]
597 fn resource_loader_new() {
598 let _loader = ResourceLoader::new();
599 }
600
601 #[test]
602 fn resource_loader_default() {
603 let _loader = ResourceLoader::default();
604 }
605
606 // -----------------------------------------------------------------------
607 // URL resolution
608 // -----------------------------------------------------------------------
609
610 #[test]
611 fn fetch_url_invalid_url_error() {
612 let mut loader = ResourceLoader::new();
613 let result = loader.fetch_url("not a url at all", None);
614 assert!(matches!(result, Err(LoadError::InvalidUrl(_))));
615 }
616
617 #[test]
618 fn fetch_url_relative_without_base_errors() {
619 let mut loader = ResourceLoader::new();
620 let result = loader.fetch_url("/relative/path", None);
621 assert!(matches!(result, Err(LoadError::InvalidUrl(_))));
622 }
623
624 #[test]
625 fn fetch_url_relative_with_base_resolves() {
626 let mut loader = ResourceLoader::new();
627 let base = Url::parse("http://example.com/page").unwrap();
628 // This will fail since we can't actually connect in tests,
629 // but the URL resolution itself should work (it won't be InvalidUrl).
630 let result = loader.fetch_url("/style.css", Some(&base));
631 assert!(result.is_err());
632 // The error should NOT be InvalidUrl — the URL resolved successfully.
633 assert!(!matches!(result, Err(LoadError::InvalidUrl(_))));
634 }
635
636 // -----------------------------------------------------------------------
637 // Data URL loading
638 // -----------------------------------------------------------------------
639
640 #[test]
641 fn data_url_plain_text() {
642 let mut loader = ResourceLoader::new();
643 let result = loader.fetch_url("data:text/plain,Hello%20World", None);
644 assert!(result.is_ok());
645 match result.unwrap() {
646 Resource::Other {
647 data, mime_type, ..
648 } => {
649 assert_eq!(mime_type, "text/plain");
650 assert_eq!(String::from_utf8(data).unwrap(), "Hello World");
651 }
652 other => panic!("expected Other, got {:?}", other),
653 }
654 }
655
656 #[test]
657 fn data_url_html() {
658 let mut loader = ResourceLoader::new();
659 let result = loader.fetch_url("data:text/html,<h1>Hello</h1>", None);
660 assert!(result.is_ok());
661 match result.unwrap() {
662 Resource::Html { text, .. } => {
663 assert_eq!(text, "<h1>Hello</h1>");
664 }
665 other => panic!("expected Html, got {:?}", other),
666 }
667 }
668
669 #[test]
670 fn data_url_css() {
671 let mut loader = ResourceLoader::new();
672 let result = loader.fetch_url("data:text/css,body{color:red}", None);
673 assert!(result.is_ok());
674 match result.unwrap() {
675 Resource::Css { text, .. } => {
676 assert_eq!(text, "body{color:red}");
677 }
678 other => panic!("expected Css, got {:?}", other),
679 }
680 }
681
682 #[test]
683 fn data_url_image() {
684 let mut loader = ResourceLoader::new();
685 let result = loader.fetch_url("data:image/png;base64,/wCq", None);
686 assert!(result.is_ok());
687 match result.unwrap() {
688 Resource::Image {
689 data, mime_type, ..
690 } => {
691 assert_eq!(mime_type, "image/png");
692 assert_eq!(data, vec![0xFF, 0x00, 0xAA]);
693 }
694 other => panic!("expected Image, got {:?}", other),
695 }
696 }
697
698 #[test]
699 fn data_url_base64() {
700 let mut loader = ResourceLoader::new();
701 let result = loader.fetch_url("data:text/plain;base64,SGVsbG8=", None);
702 assert!(result.is_ok());
703 match result.unwrap() {
704 Resource::Other { data, .. } => {
705 assert_eq!(String::from_utf8(data).unwrap(), "Hello");
706 }
707 other => panic!("expected Other, got {:?}", other),
708 }
709 }
710
711 #[test]
712 fn data_url_empty() {
713 let mut loader = ResourceLoader::new();
714 let result = loader.fetch_url("data:,", None);
715 assert!(result.is_ok());
716 }
717
718 #[test]
719 fn data_url_via_fetch_method() {
720 let mut loader = ResourceLoader::new();
721 let url = Url::parse("data:text/plain,Hello").unwrap();
722 let result = loader.fetch(&url);
723 assert!(result.is_ok());
724 match result.unwrap() {
725 Resource::Other { data, .. } => {
726 assert_eq!(String::from_utf8(data).unwrap(), "Hello");
727 }
728 other => panic!("expected Other, got {:?}", other),
729 }
730 }
731
732 #[test]
733 fn data_url_invalid() {
734 let mut loader = ResourceLoader::new();
735 let result = loader.fetch_url("data:text/plain", None);
736 assert!(matches!(result, Err(LoadError::InvalidUrl(_))));
737 }
738
739 #[test]
740 fn data_url_binary() {
741 let mut loader = ResourceLoader::new();
742 let result = loader.fetch_url("data:application/octet-stream;base64,/wCq", None);
743 assert!(result.is_ok());
744 match result.unwrap() {
745 Resource::Other {
746 data, mime_type, ..
747 } => {
748 assert_eq!(mime_type, "application/octet-stream");
749 assert_eq!(data, vec![0xFF, 0x00, 0xAA]);
750 }
751 other => panic!("expected Other, got {:?}", other),
752 }
753 }
754
755 // -----------------------------------------------------------------------
756 // about: URL loading
757 // -----------------------------------------------------------------------
758
759 #[test]
760 fn about_blank_via_fetch_url() {
761 let mut loader = ResourceLoader::new();
762 let result = loader.fetch_url("about:blank", None);
763 assert!(result.is_ok());
764 match result.unwrap() {
765 Resource::Html {
766 text,
767 encoding,
768 base_url,
769 ..
770 } => {
771 assert_eq!(text, ABOUT_BLANK_HTML);
772 assert_eq!(encoding, Encoding::Utf8);
773 assert_eq!(base_url.scheme(), "about");
774 }
775 other => panic!("expected Html, got {:?}", other),
776 }
777 }
778
779 #[test]
780 fn about_blank_via_fetch() {
781 let mut loader = ResourceLoader::new();
782 let url = Url::parse("about:blank").unwrap();
783 let result = loader.fetch(&url);
784 assert!(result.is_ok());
785 match result.unwrap() {
786 Resource::Html {
787 text,
788 encoding,
789 base_url,
790 ..
791 } => {
792 assert_eq!(text, ABOUT_BLANK_HTML);
793 assert_eq!(encoding, Encoding::Utf8);
794 assert_eq!(base_url.scheme(), "about");
795 }
796 other => panic!("expected Html, got {:?}", other),
797 }
798 }
799
800 #[test]
801 fn about_blank_dom_structure() {
802 let doc = we_html::parse_html(ABOUT_BLANK_HTML);
803
804 // Find the <html> element under the document root.
805 let html = doc
806 .children(doc.root())
807 .find(|&n| doc.tag_name(n) == Some("html"));
808 assert!(html.is_some(), "document should have an <html> element");
809 let html = html.unwrap();
810
811 // The DOM should have html > head + body structure.
812 let children: Vec<_> = doc
813 .children(html)
814 .filter(|&n| doc.tag_name(n).is_some())
815 .collect();
816 assert_eq!(children.len(), 2);
817 assert_eq!(doc.tag_name(children[0]).unwrap(), "head");
818 assert_eq!(doc.tag_name(children[1]).unwrap(), "body");
819
820 // Body should have no child elements.
821 let body_children: Vec<_> = doc
822 .children(children[1])
823 .filter(|&n| doc.tag_name(n).is_some())
824 .collect();
825 assert!(body_children.is_empty());
826 }
827
828 #[test]
829 fn about_unsupported_url() {
830 let mut loader = ResourceLoader::new();
831 let result = loader.fetch_url("about:invalid", None);
832 assert!(matches!(result, Err(LoadError::InvalidUrl(_))));
833 }
834}