use super::*;
#[test]
fn test_parse_mentions() {
let text = "Hey @alice.bsky.social check this out";
let builder = RichText::parse(text);
assert_eq!(builder.facet_candidates.len(), 1);
match &builder.facet_candidates[0] {
FacetCandidate::Mention { range, .. } => {
assert_eq!(&builder.text[range.clone()], "@alice.bsky.social");
}
_ => panic!("Expected mention facet"),
}
}
#[test]
fn test_parse_links() {
let text = "Check out https://example.com for more info";
let builder = RichText::parse(text);
assert!(builder.facet_candidates.iter().any(|fc| {
matches!(fc, FacetCandidate::Link { range } if text[range.clone()].contains("example.com"))
}));
}
#[test]
fn test_parse_tags() {
let text = "This is #cool and #awesome";
let builder = RichText::parse(text);
let tags: Vec<_> = builder
.facet_candidates
.iter()
.filter_map(|fc| match fc {
FacetCandidate::Tag { range } => Some(&builder.text[range.clone()]),
_ => None,
})
.collect();
assert!(tags.contains(&"#cool"));
assert!(tags.contains(&"#awesome"));
}
#[test]
fn test_markdown_links() {
let text = "Check out [this link](https://example.com)";
let builder = RichText::parse(text);
assert!(builder.text.contains("this link"));
assert!(!builder.text.contains("["));
assert!(!builder.text.contains("]"));
assert!(builder.facet_candidates.iter().any(|fc| matches!(
fc,
FacetCandidate::MarkdownLink { url, .. } if url == "https://example.com"
)));
}
#[test]
#[cfg(feature = "api_bluesky")]
fn test_builder_manual_construction() {
let did = crate::types::did::Did::new_static("did:plc:z72i7hdynmk6r22z27h6tvur").unwrap();
let result = RichText::builder()
.text("Hello @alice check out example.com".to_string())
.mention(&did, 6..12)
.link("https://example.com", Some(23..34))
.build()
.unwrap();
assert_eq!(result.text.as_str(), "Hello @alice check out example.com");
assert!(result.facets.is_some());
let facets = result.facets.unwrap();
assert_eq!(facets.len(), 2);
}
#[test]
#[cfg(feature = "api_bluesky")]
fn test_overlapping_facets_error() {
let did1 = crate::types::did::Did::new_static("did:plc:z72i7hdynmk6r22z27h6tvur").unwrap();
let did2 = crate::types::did::Did::new_static("did:plc:ewvi7nxzyoun6zhxrhs64oiz").unwrap();
let result = RichText::builder()
.text("Hello world".to_string())
.mention(&did1, 0..5)
.mention(&did2, 3..8) .build();
assert!(matches!(
result,
Err(RichTextError::OverlappingFacets(_, _))
));
}
#[test]
fn test_parse_did_mentions() {
let text = "Hey @did:plc:z72i7hdynmk6r22z27h6tvur check this out";
let builder = RichText::parse(text);
assert_eq!(builder.facet_candidates.len(), 1);
match &builder.facet_candidates[0] {
FacetCandidate::Mention { range, did } => {
assert_eq!(&text[range.clone()], "@did:plc:z72i7hdynmk6r22z27h6tvur");
assert!(did.is_some()); }
_ => panic!("Expected mention facet"),
}
}
#[test]
fn test_bare_domain_link() {
let text = "Visit example.com for info";
let builder = RichText::parse(text);
assert!(builder.facet_candidates.iter().any(|fc| {
matches!(fc, FacetCandidate::Link { range } if text[range.clone()].contains("example.com"))
}));
}
#[test]
fn test_trailing_punctuation_stripped() {
let text = "Check https://example.com, and https://test.org.";
let builder = RichText::parse(text);
let link_count = builder
.facet_candidates
.iter()
.filter(|fc| matches!(fc, FacetCandidate::Link { .. }))
.count();
assert_eq!(link_count, 2);
for fc in &builder.facet_candidates {
if let FacetCandidate::Link { range } = fc {
let url = &text[range.clone()];
assert!(!url.ends_with(','));
assert!(!url.ends_with('.'));
}
}
}
#[test]
#[cfg(feature = "api_bluesky")]
fn test_embed_detection_external() {
let text = "Check out https://external.com/article";
let builder = RichText::parse(text);
assert!(builder.embed_candidates.is_some());
let embeds = builder.embed_candidates.unwrap();
assert_eq!(embeds.len(), 1);
match &embeds[0] {
EmbedCandidate::External { url, metadata } => {
assert!(url.contains("external.com"));
assert!(metadata.is_none());
}
_ => panic!("Expected external embed"),
}
}
#[test]
#[cfg(feature = "api_bluesky")]
fn test_embed_detection_bsky_post() {
let text = "See https://bsky.app/profile/alice.bsky.social/post/abc123";
let builder = RichText::parse(text);
assert!(builder.embed_candidates.is_some());
let embeds = builder.embed_candidates.unwrap();
assert_eq!(embeds.len(), 1);
match &embeds[0] {
EmbedCandidate::Record { at_uri, .. } => {
assert_eq!(
at_uri.as_str(),
"at://alice.bsky.social/app.bsky.feed.post/abc123"
);
}
_ => panic!("Expected record embed"),
}
}
#[test]
#[cfg(feature = "api_bluesky")]
fn test_markdown_link_with_embed() {
let text = "Read [my post](https://bsky.app/profile/me.bsky.social/post/xyz)";
let builder = RichText::parse(text);
assert!(
builder
.facet_candidates
.iter()
.any(|fc| matches!(fc, FacetCandidate::MarkdownLink { .. }))
);
assert!(builder.embed_candidates.is_some());
let embeds = builder.embed_candidates.unwrap();
assert_eq!(embeds.len(), 1);
}
#[test]
fn test_sanitize_soft_hyphen() {
let text = "Hello\u{00AD}World";
let builder = RichText::parse(text);
assert_eq!(builder.text, "HelloWorld");
}
#[test]
fn test_sanitize_zero_width_space() {
let text = "Hello\u{200B}World";
let builder = RichText::parse(text);
assert_eq!(builder.text, "HelloWorld");
}
#[test]
fn test_sanitize_normalize_newlines() {
let text = "Hello\r\nWorld";
let builder = RichText::parse(text);
assert_eq!(builder.text, "Hello\nWorld");
}
#[test]
fn test_sanitize_collapse_multiple_newlines() {
let text = "Hello\n\n\n\nWorld";
let builder = RichText::parse(text);
assert_eq!(builder.text, "Hello\n\nWorld");
}
#[test]
fn test_sanitize_mixed_invisible_and_newlines() {
let text = "Hello\u{200B}\n\u{200C}\n\u{00AD}World";
let builder = RichText::parse(text);
assert_eq!(builder.text, "Hello\n\nWorld");
}
#[test]
fn test_sanitize_preserves_facets() {
let text = "Hey @alice.bsky.social\u{200B} check\u{00AD}out https://example.com";
let builder = RichText::parse(text);
assert!(
builder
.facet_candidates
.iter()
.any(|fc| matches!(fc, FacetCandidate::Mention { .. }))
);
assert!(
builder
.facet_candidates
.iter()
.any(|fc| matches!(fc, FacetCandidate::Link { .. }))
);
}
#[test]
fn test_sanitize_newlines_with_spaces() {
let text = "Hello\n \n \nWorld";
let builder = RichText::parse(text);
assert_eq!(builder.text, "Hello\n\nWorld");
}
#[test]
fn test_sanitize_preserves_no_excess_newlines() {
let text = "Hello\nWorld";
let builder = RichText::parse(text);
assert_eq!(builder.text, "Hello\nWorld");
}
#[test]
fn test_sanitize_empty_input() {
let text = "";
let builder = RichText::parse(text);
assert_eq!(builder.text, "");
}
#[test]
fn test_sanitize_only_invisible_chars() {
let text = "\u{200B}\u{200C}\u{200D}\u{00AD}";
let builder = RichText::parse(text);
assert_eq!(builder.text, "");
}
#[test]
fn test_sanitize_cr_normalization() {
let text = "Hello\rWorld";
let builder = RichText::parse(text);
assert_eq!(builder.text, "Hello\nWorld");
}
#[test]
fn test_sanitize_mixed_line_endings() {
let text = "Line1\r\nLine2\rLine3\nLine4";
let builder = RichText::parse(text);
assert_eq!(builder.text, "Line1\nLine2\nLine3\nLine4");
}
#[test]
fn test_sanitize_preserves_regular_spaces() {
let text = "Hello World";
let builder = RichText::parse(text);
assert_eq!(builder.text, "Hello World");
}
#[test]
fn test_tag_too_long() {
let long_tag = "a".repeat(65);
let text = format!("#{}", long_tag);
let builder = RichText::parse(text);
assert!(
builder
.facet_candidates
.iter()
.all(|fc| !matches!(fc, FacetCandidate::Tag { .. }))
);
}
#[test]
fn test_tag_with_zero_width_chars() {
let text = "This is #cool\u{200B}tag";
let builder = RichText::parse(text);
let tags: Vec<_> = builder
.facet_candidates
.iter()
.filter_map(|fc| match fc {
FacetCandidate::Tag { range } => Some(&builder.text[range.clone()]),
_ => None,
})
.collect();
assert!(tags.iter().any(|t| t.starts_with("#cool")));
}
#[test]
fn test_url_with_parens() {
let text = "See https://en.wikipedia.org/wiki/Rust_(programming_language)";
let builder = RichText::parse(text);
assert!(builder.facet_candidates.iter().any(|fc| {
matches!(fc, FacetCandidate::Link { range } if text[range.clone()].contains("programming_language"))
}));
}
#[test]
fn test_markdown_link_unclosed() {
let text = "This is [unclosed link";
let builder = RichText::parse(text);
assert_eq!(builder.text, text);
assert!(
builder
.facet_candidates
.iter()
.all(|fc| !matches!(fc, FacetCandidate::MarkdownLink { .. }))
);
}
#[test]
fn test_nested_markdown_attempts() {
let text = "[[nested](https://inner.com)](https://outer.com)";
let builder = RichText::parse(text);
let markdown_count = builder
.facet_candidates
.iter()
.filter(|fc| matches!(fc, FacetCandidate::MarkdownLink { .. }))
.count();
assert!(markdown_count > 0);
}
#[test]
fn test_mention_with_emoji() {
let text = "Hey @aliceπ.bsky.social wassup";
let builder = RichText::parse(text);
let mentions: Vec<_> = builder
.facet_candidates
.iter()
.filter_map(|fc| match fc {
FacetCandidate::Mention { range, .. } => Some(&text[range.clone()]),
_ => None,
})
.collect();
for mention in mentions {
assert!(!mention.contains('π'));
}
}
#[test]
fn test_handle_with_trailing_dots() {
let text = "Hey @alice.bsky.social... how are you";
let builder = RichText::parse(text);
if let Some(FacetCandidate::Mention { range, .. }) = builder.facet_candidates.first() {
let mention = &text[range.clone()];
assert!(!mention.ends_with('.'));
}
}
#[test]
fn test_url_javascript_protocol() {
let text = "Click javascript:alert(1) or data:text/html,<script>alert(1)</script>";
let builder = RichText::parse(text);
for fc in &builder.facet_candidates {
if let FacetCandidate::Link { range } = fc {
let url = &text[range.clone()];
assert!(!url.starts_with("javascript:"));
assert!(!url.starts_with("data:"));
}
}
}
#[test]
fn test_extremely_long_url() {
let long_path = "a/".repeat(1000);
let text = format!("Visit https://example.com/{}", long_path);
let builder = RichText::parse(text);
assert!(
builder
.facet_candidates
.iter()
.any(|fc| matches!(fc, FacetCandidate::Link { .. }))
);
}
#[test]
fn test_empty_string() {
let text = "";
let builder = RichText::parse(text);
assert_eq!(builder.text, "");
assert!(builder.facet_candidates.is_empty());
}
#[test]
fn test_only_whitespace() {
let text = " \t\n ";
let builder = RichText::parse(text);
assert!(builder.facet_candidates.is_empty());
}
#[test]
fn test_markdown_with_newlines() {
let text = "This is [text\nwith](https://example.com) newline";
let builder = RichText::parse(text);
let _ = builder.facet_candidates;
}
#[test]
fn test_multiple_at_signs() {
let text = "Hey @@alice.bsky.social";
let builder = RichText::parse(text);
for fc in &builder.facet_candidates {
if let FacetCandidate::Mention { range, .. } = fc {
assert!(range.end <= text.len());
let _ = &text[range.clone()]; }
}
}
#[test]
fn test_url_with_unicode_domain() {
let text = "Visit https://δΎγ.jp for info";
let builder = RichText::parse(text);
let _ = builder.facet_candidates;
}
#[test]
#[cfg(feature = "api_bluesky")]
fn test_build_with_invalid_range() {
let did = crate::types::did::Did::new_static("did:plc:z72i7hdynmk6r22z27h6tvur").unwrap();
let result = RichText::builder()
.text("Short".to_string())
.mention(&did, 0..100)
.build();
assert!(matches!(result, Err(RichTextError::InvalidRange { .. })));
}
#[test]
fn test_rtl_override_injection() {
let text = "Hey @alice\u{202E}reversed\u{202C}.bsky.social";
let builder = RichText::parse(text);
let _ = builder.facet_candidates;
}
#[test]
fn test_tag_empty_after_hash() {
let text = "This is # a test";
let builder = RichText::parse(text);
assert!(
builder
.facet_candidates
.iter()
.all(|fc| !matches!(fc, FacetCandidate::Tag { .. }))
);
}
#[test]
fn test_facet_ranges_valid_utf8_boundaries() {
let text = "Hey @alice.bsky.social check δ½ ε₯½ #tagπ₯ and https://example.com/ζ΅θ―";
let builder = RichText::parse(text);
for fc in &builder.facet_candidates {
let range = match fc {
FacetCandidate::Mention { range, .. } => range,
FacetCandidate::Link { range } => range,
FacetCandidate::Tag { range } => range,
FacetCandidate::MarkdownLink { display_range, .. } => display_range,
};
let slice = &builder.text[range.clone()];
assert!(std::str::from_utf8(slice.as_bytes()).is_ok());
}
}
#[test]
fn test_emoji_grapheme_clusters() {
let text = "Hello π¨βπ©βπ§βπ§ @alice.bsky.social";
let builder = RichText::parse(text);
assert!(
builder
.facet_candidates
.iter()
.any(|fc| matches!(fc, FacetCandidate::Mention { .. }))
);
for fc in &builder.facet_candidates {
if let FacetCandidate::Mention { range, .. } = fc {
let _ = &builder.text[range.clone()]; }
}
}
#[test]
fn test_tag_with_emoji() {
let text = "This is #coolπ₯";
let builder = RichText::parse(text);
let tags: Vec<_> = builder
.facet_candidates
.iter()
.filter_map(|fc| match fc {
FacetCandidate::Tag { range } => Some(&builder.text[range.clone()]),
_ => None,
})
.collect();
assert!(tags.iter().any(|t| t.contains("π₯")));
}
#[test]
fn test_sanitize_newlines_with_emoji() {
let text = "Hello π\n\n\n\nWorld π";
let builder = RichText::parse(text);
assert_eq!(builder.text, "Hello π\n\nWorld π");
}