use rustbot::i18n::Locale; use rustbot::karma_web::{parse_board, parse_events, parse_meta, render, render_json, Channel}; fn board_with_events(net: &str, store: &str, log: &str) -> rustbot::karma_web::Board { let mut b = parse_board(net, store); b.events = parse_events(log); b } #[test] fn parses_flat_number_format() { let b = parse_board("libera", r#"{"romaka":2}"#); assert_eq!(b.rows.len(), 1); assert_eq!(b.rows[0].thing, "romaka"); assert_eq!(b.rows[0].count, 2); } #[test] fn parses_object_format_with_reason_and_by() { let b = parse_board("t", r#"{"rust":{"n":5,"why":"clean PR","by":"alice"}}"#); assert_eq!(b.rows[0].count, 5); assert_eq!(b.rows[0].reason.as_deref(), Some("clean PR")); assert_eq!(b.rows[0].by.as_deref(), Some("alice")); } #[test] fn sorts_descending_by_count() { let b = parse_board("t", r#"{"a":1,"b":9,"c":-3}"#); let order: Vec<&str> = b.rows.iter().map(|r| r.thing.as_str()).collect(); assert_eq!(order, vec!["b", "a", "c"]); } #[test] fn parses_event_log_lines() { let log = "{\"t\":100,\"o\":\"rust\",\"d\":1,\"by\":\"alice\",\"why\":\"clean PR\"}\n\ {\"t\":200,\"o\":\"rust\",\"d\":-1,\"by\":\"bob\"}\n\ \n\ garbage line\n"; let ev = parse_events(log); assert_eq!(ev.len(), 2); assert_eq!(ev[0].thing, "rust"); assert_eq!(ev[0].delta, 1); assert_eq!(ev[0].by.as_deref(), Some("alice")); assert_eq!(ev[0].why.as_deref(), Some("clean PR")); assert_eq!(ev[1].delta, -1); assert!(ev[1].why.is_none()); } #[test] fn renders_hero_podium_and_scores() { let b = parse_board( "libera", r#"{"rust":{"n":5,"why":"clean PR","by":"alice"}}"#, ); let html = render(&[b], 0, Locale::En); assert!(html.contains(">karma<"), "wordmark missing"); assert!(html.contains("podium"), "no podium"); assert!(html.contains(">rust<"), "champion nick missing"); assert!(html.contains(">+5<"), "score missing"); assert!(html.contains("clean PR"), "reason missing"); } #[test] fn renders_activity_and_givers_from_events() { let log = "{\"t\":100,\"o\":\"rust\",\"d\":1,\"by\":\"alice\",\"why\":\"clean PR\"}\n\ {\"t\":200,\"o\":\"docs\",\"d\":1,\"by\":\"alice\"}\n"; let b = board_with_events("libera", r#"{"rust":1,"docs":1}"#, log); let html = render(&[b], 0, Locale::En); assert!(html.contains("recent activity"), "no activity section"); assert!(html.contains("top givers"), "no givers section"); assert!(html.contains("gave"), "no feed verb"); assert!(html.contains("2 gifts"), "giver count wrong"); assert!(html.contains("data-t=\"200\""), "feed newest-first missing"); } #[test] fn empty_states_are_friendly() { let html = render(&[], 0, Locale::En); assert!(html.contains("No karma yet"), "{html}"); let b = parse_board("libera", r#"{"romaka":2}"#); let html = render(&[b], 0, Locale::En); assert!(html.contains("Nothing yet"), "empty activity state missing"); assert!(html.contains("Be the first"), "empty givers state missing"); } #[test] fn renders_spanish_chrome() { let log = "{\"t\":100,\"o\":\"rust\",\"d\":1,\"by\":\"nadia\",\"why\":\"buen PR\"}\n"; let b = board_with_events("libera", r#"{"rust":1}"#, log); let html = render(&[b], 0, Locale::Es); assert!(html.contains("karma neto"), "es stat label missing"); assert!( html.contains("actividad reciente"), "es activity heading missing" ); assert!(html.contains("top donantes"), "es givers heading missing"); assert!(html.contains("le dio a"), "es feed verb missing"); // relative-time config for the client is Spanish assert!(html.contains("\"pre\":\"hace \""), "es time config missing"); // user content is never translated assert!(html.contains("buen PR") && html.contains(">rust<")); } #[test] fn parses_meta_channels_topic_and_users() { let m = parse_meta( r##"{"server":"irc.libera.chat","webchat":"https://web.libera.chat/","seen":123,"channels":[{"name":"#argentina","topic":"Bienvenidos","users":42},{"name":"#ar"}]}"##, ); assert_eq!(m.server, "irc.libera.chat"); assert_eq!(m.webchat, "https://web.libera.chat/"); assert_eq!(m.seen, 123); assert_eq!(m.channels.len(), 2); assert_eq!(m.channels[0].name, "#argentina"); assert_eq!(m.channels[0].topic.as_deref(), Some("Bienvenidos")); assert_eq!(m.channels[0].users, Some(42)); assert!(m.channels[1].topic.is_none() && m.channels[1].users.is_none()); // old bare-string format still parses let old = parse_meta(r##"{"server":"x","channels":["#c"]}"##); assert_eq!(old.channels[0].name, "#c"); // garbage degrades to empty let g = parse_meta("not json"); assert!(g.server.is_empty() && g.channels.is_empty()); } #[test] fn renders_source_topic_users_join_and_online() { let mut b = parse_board("libera", r#"{"romaka":2}"#); b.server = "irc.libera.chat".to_string(); b.webchat = "https://web.libera.chat/".to_string(); b.seen = 1000; b.channels = vec![Channel { name: "#argentina".to_string(), topic: Some("Argentina en Libera - stats https://qadlo.example/".to_string()), users: Some(42), }]; let en = render(&[b], 1100, Locale::En); // now close to seen -> online assert!(en.contains("#argentina"), "channel missing"); assert!(en.contains("irc.libera.chat"), "server missing"); assert!(en.contains("Argentina en Libera"), "topic missing"); assert!( en.contains("evil javascript:alert(1)".to_string()), users: None, }]; let html = render(&[b], 0, Locale::En); assert!(!html.contains("