From b5f8e273a98271ac2e97b4a58727aa202016202f Mon Sep 17 00:00:00 2001 From: wangdl Date: Sat, 30 May 2026 21:18:30 +0800 Subject: [PATCH] feat: ReadingEvent serde tests + NoteAnchor with from_position constructor and tests --- crates/zx_document_core/src/anchors.rs | 79 +++++++++++++++++++++++++ crates/zx_document_core/src/events.rs | 80 +++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 1 deletion(-) diff --git a/crates/zx_document_core/src/anchors.rs b/crates/zx_document_core/src/anchors.rs index eb35ce7..e70a963 100644 --- a/crates/zx_document_core/src/anchors.rs +++ b/crates/zx_document_core/src/anchors.rs @@ -29,3 +29,82 @@ pub enum NoteAnchor { knowledge_item_id: String, }, } + +impl NoteAnchor { + /// Create a NoteAnchor from a material_id and optional ReadingPosition. + pub fn from_position(material_id: &str, position: Option<&crate::progress::ReadingPosition>) -> Self { + match position { + Some(crate::progress::ReadingPosition::Markdown { block_id, .. }) => { + NoteAnchor::MarkdownBlock { + material_id: material_id.to_string(), + block_id: block_id.clone(), + } + } + Some(crate::progress::ReadingPosition::Text { line_number, .. }) => { + NoteAnchor::TextLine { + material_id: material_id.to_string(), + line_number: *line_number, + } + } + Some(crate::progress::ReadingPosition::Pdf { page_number, .. }) => { + NoteAnchor::PdfPage { + material_id: material_id.to_string(), + page_number: *page_number, + } + } + Some(crate::progress::ReadingPosition::Image { .. }) => NoteAnchor::Image { + material_id: material_id.to_string(), + }, + Some(crate::progress::ReadingPosition::Epub { chapter_id, .. }) => { + NoteAnchor::EpubChapter { + material_id: material_id.to_string(), + chapter_id: chapter_id.clone(), + } + } + _ => NoteAnchor::Material { + material_id: material_id.to_string(), + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::progress::ReadingPosition; + + #[test] + fn test_material_anchor() { + let a = NoteAnchor::from_position("abc", None); + assert_eq!(a, NoteAnchor::Material { material_id: "abc".into() }); + } + + #[test] + fn test_markdown_anchor() { + let pos = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 }; + let a = NoteAnchor::from_position("abc", Some(&pos)); + assert_eq!(a, NoteAnchor::MarkdownBlock { material_id: "abc".into(), block_id: "h1".into() }); + } + + #[test] + fn test_pdf_anchor() { + let pos = ReadingPosition::Pdf { page_number: 3, page_progress: 0.5, overall_progress: 0.1 }; + let a = NoteAnchor::from_position("abc", Some(&pos)); + assert_eq!(a, NoteAnchor::PdfPage { material_id: "abc".into(), page_number: 3 }); + } + + #[test] + fn test_anchor_serde() { + let a = NoteAnchor::MarkdownBlock { material_id: "abc".into(), block_id: "h1".into() }; + let json = serde_json::to_string(&a).unwrap(); + let back: NoteAnchor = serde_json::from_str(&json).unwrap(); + assert_eq!(back, a); + } + + #[test] + fn test_unknown_position_falls_back_to_material() { + let pos = ReadingPosition::Unknown; + let a = NoteAnchor::from_position("abc", Some(&pos)); + assert_eq!(a, NoteAnchor::Material { material_id: "abc".into() }); + } +} diff --git a/crates/zx_document_core/src/events.rs b/crates/zx_document_core/src/events.rs index ffab57c..beea97f 100644 --- a/crates/zx_document_core/src/events.rs +++ b/crates/zx_document_core/src/events.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::progress::ReadingPosition; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type")] pub enum ReadingEvent { MaterialOpened { @@ -30,3 +30,81 @@ pub enum ReadingEvent { timestamp_ms: i64, }, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_material_opened_serde() { + let e = ReadingEvent::MaterialOpened { + material_id: "abc".into(), + timestamp_ms: 1000, + }; + let json = serde_json::to_string(&e).unwrap(); + assert!(json.contains("\"type\":\"MaterialOpened\"")); + let back: ReadingEvent = serde_json::from_str(&json).unwrap(); + assert_eq!(back, e); + } + + #[test] + fn test_material_closed_serde() { + let e = ReadingEvent::MaterialClosed { + material_id: "abc".into(), + timestamp_ms: 2000, + active_seconds: 120, + }; + let json = serde_json::to_string(&e).unwrap(); + let back: ReadingEvent = serde_json::from_str(&json).unwrap(); + assert_eq!(back, e); + } + + #[test] + fn test_position_changed_serde() { + let e = ReadingEvent::PositionChanged { + material_id: "abc".into(), + position: ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 }, + timestamp_ms: 3000, + }; + let json = serde_json::to_string(&e).unwrap(); + let back: ReadingEvent = serde_json::from_str(&json).unwrap(); + assert_eq!(back, e); + } + + #[test] + fn test_heartbeat_serde() { + let e = ReadingEvent::Heartbeat { + material_id: "abc".into(), + active_seconds: 15, + position: None, + timestamp_ms: 4000, + }; + let json = serde_json::to_string(&e).unwrap(); + let back: ReadingEvent = serde_json::from_str(&json).unwrap(); + assert_eq!(back, e); + } + + #[test] + fn test_heartbeat_with_position_serde() { + let e = ReadingEvent::Heartbeat { + material_id: "abc".into(), + active_seconds: 15, + position: Some(ReadingPosition::Pdf { page_number: 3, page_progress: 0.5, overall_progress: 0.1 }), + timestamp_ms: 5000, + }; + let json = serde_json::to_string(&e).unwrap(); + let back: ReadingEvent = serde_json::from_str(&json).unwrap(); + assert_eq!(back, e); + } + + #[test] + fn test_marked_as_read_serde() { + let e = ReadingEvent::MarkedAsRead { + material_id: "abc".into(), + timestamp_ms: 6000, + }; + let json = serde_json::to_string(&e).unwrap(); + let back: ReadingEvent = serde_json::from_str(&json).unwrap(); + assert_eq!(back, e); + } +}