| 1 | use onestore::{ |
| 2 | ExGuid, PageCreation, RevisionIndex, Store, |
| 3 | document::{Document, Kind}, |
| 4 | op::{PageOp, SectionOp}, |
| 5 | page::{Page, PageObject}, |
| 6 | }; |
| 7 | |
| 8 | #[path = "support/current.rs"] |
| 9 | mod current; |
| 10 | #[path = "support/disk.rs"] |
| 11 | mod disk; |
| 12 | #[path = "support/ops.rs"] |
| 13 | mod ops; |
| 14 | |
| 15 | const SOURCE: &[u8] = |
| 16 | include_bytes!("../../../corpus/page-lifecycle/03-renamed/notebook/Lifecycle.one"); |
| 17 | |
| 18 | #[test] |
| 19 | fn native_section_accepts_empty_and_titled_pages_with_preserved_history() { |
| 20 | let original = Store::parse(SOURCE).unwrap(); |
| 21 | let old_index = RevisionIndex::parse(&original).unwrap(); |
| 22 | let original_pages = Document::parse(&old_index).unwrap().pages().unwrap(); |
| 23 | let mut expected = original_pages.clone(); |
| 24 | let mut source = SOURCE.to_vec(); |
| 25 | for (ordinal, title) in [None, Some(""), Some("New 🦋 é"), Some("New 🦋 é")] |
| 26 | .into_iter() |
| 27 | .enumerate() |
| 28 | { |
| 29 | let before = (ordinal == 1).then_some(original_pages[0].0); |
| 30 | let intent = PageCreation::new(before, title, "Rust author").unwrap(); |
| 31 | let serialized = serde_json::to_vec(&intent).unwrap(); |
| 32 | let retained = serde_json::from_slice(&serialized).unwrap(); |
| 33 | assert_eq!(intent, retained); |
| 34 | let prepared = ops::section_op(&source, SectionOp::Create(retained.clone())).unwrap(); |
| 35 | let store = Store::parse(prepared.as_bytes()).unwrap(); |
| 36 | assert_eq!( |
| 37 | store.header.transaction_count, |
| 38 | original.header.transaction_count + ordinal as u32 + 1 |
| 39 | ); |
| 40 | assert_eq!(store.header.file_id, original.header.file_id); |
| 41 | let index = RevisionIndex::parse(&store).unwrap(); |
| 42 | index.validate_current().unwrap(); |
| 43 | let document = Document::parse(&index).unwrap(); |
| 44 | let at = if ordinal == 1 { 0 } else { expected.len() }; |
| 45 | expected.insert(at, (intent.space(), intent.object())); |
| 46 | assert_eq!(document.pages().unwrap(), expected); |
| 47 | let view = &document.spaces[&intent.space()]; |
| 48 | let view = &view.revisions[&view.contexts[&ExGuid::default()]]; |
| 49 | assert!(view.nodes[&intent.object()].children.is_empty()); |
| 50 | assert_eq!( |
| 51 | view.nodes[&intent.object()].structure.len(), |
| 52 | usize::from(title.is_some()) |
| 53 | ); |
| 54 | let Kind::Metadata { |
| 55 | title: actual, |
| 56 | level, |
| 57 | } = &view.nodes[&view.roots[&2]].kind |
| 58 | else { |
| 59 | panic!() |
| 60 | }; |
| 61 | assert_eq!(actual.as_deref(), Some(title.unwrap_or_default())); |
| 62 | assert_eq!(*level, Some(1)); |
| 63 | if let Some(text) = intent.title_object() { |
| 64 | let Kind::RichText { |
| 65 | text, boilerplate, .. |
| 66 | } = &view.nodes[&text].kind |
| 67 | else { |
| 68 | panic!() |
| 69 | }; |
| 70 | assert_eq!(text, title.unwrap()); |
| 71 | assert!(!boilerplate); |
| 72 | } |
| 73 | for (sid, space) in &old_index.spaces { |
| 74 | for rid in space.revisions.keys() { |
| 75 | assert_eq!( |
| 76 | format!("{:?}", old_index.resolve(*sid, *rid).unwrap()), |
| 77 | format!("{:?}", index.resolve(*sid, *rid).unwrap()) |
| 78 | ); |
| 79 | } |
| 80 | if *sid != old_index.root { |
| 81 | assert_eq!(space.labels, index.spaces[sid].labels); |
| 82 | } |
| 83 | } |
| 84 | assert!(ops::section_op(prepared.as_bytes(), SectionOp::Create(intent.clone())).is_err()); |
| 85 | source = prepared.as_bytes().to_vec(); |
| 86 | } |
| 87 | if let Some(output) = std::env::var_os("ONESTORE_PAGE_CREATION_OUTPUT") { |
| 88 | std::fs::create_dir(&output).unwrap(); |
| 89 | std::fs::write(std::path::Path::new(&output).join("Lifecycle.one"), &source).unwrap(); |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | #[test] |
| 94 | fn created_pages_support_title_edits_and_body_insertion() { |
| 95 | let mut source = onestore::create_section("pages.one", "Original", "Author").unwrap(); |
| 96 | for title in [None, Some(""), Some("Explicit 🦋 é")] { |
| 97 | let intent = PageCreation::new(None, title, "Author").unwrap(); |
| 98 | source = ops::section_op(&source, SectionOp::Create(intent.clone())) |
| 99 | .unwrap() |
| 100 | .as_bytes() |
| 101 | .to_vec(); |
| 102 | let (add, ..) = ops::new_outline(36.0, 36.0, "Body 🦀 é"); |
| 103 | source = ops::page_op(&source, intent.space(), add) |
| 104 | .unwrap() |
| 105 | .as_bytes() |
| 106 | .to_vec(); |
| 107 | let store = Store::parse(&source).unwrap(); |
| 108 | let index = RevisionIndex::parse(&store).unwrap(); |
| 109 | let document = Document::parse(&index).unwrap(); |
| 110 | let view = &document.spaces[&intent.space()]; |
| 111 | let view = &view.revisions[&view.contexts[&ExGuid::default()]]; |
| 112 | let Kind::Metadata { title: actual, .. } = &view.nodes[&view.roots[&2]].kind else { |
| 113 | panic!() |
| 114 | }; |
| 115 | assert_eq!( |
| 116 | actual.as_deref(), |
| 117 | Some(title.filter(|s| !s.is_empty()).unwrap_or("Body 🦀 é")) |
| 118 | ); |
| 119 | if let Some(text) = intent.title_object() { |
| 120 | source = ops::page_op( |
| 121 | &source, |
| 122 | intent.space(), |
| 123 | PageOp::Text { |
| 124 | text, |
| 125 | range: 0..title.unwrap().encode_utf16().count() as u32, |
| 126 | with: "Renamed".into(), |
| 127 | }, |
| 128 | ) |
| 129 | .unwrap() |
| 130 | .as_bytes() |
| 131 | .to_vec(); |
| 132 | let store = Store::parse(&source).unwrap(); |
| 133 | let index = RevisionIndex::parse(&store).unwrap(); |
| 134 | let document = Document::parse(&index).unwrap(); |
| 135 | let view = &document.spaces[&intent.space()]; |
| 136 | let view = &view.revisions[&view.contexts[&ExGuid::default()]]; |
| 137 | let Kind::Metadata { title, .. } = &view.nodes[&view.roots[&2]].kind else { |
| 138 | panic!() |
| 139 | }; |
| 140 | assert_eq!(title.as_deref(), Some("Renamed")); |
| 141 | } |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | /// OneNote 2010 draws level-1 text 27 pt inside an outline whose table stops at entry 0. |
| 146 | #[test] |
| 147 | fn created_body_outlines_carry_the_indentation_table_onenote_writes() { |
| 148 | let source = onestore::create_section("pages.one", "Original", "Author").unwrap(); |
| 149 | let store = Store::parse(&source).unwrap(); |
| 150 | let index = RevisionIndex::parse(&store).unwrap(); |
| 151 | let document = Document::parse(&index).unwrap(); |
| 152 | let (space, page) = document.pages().unwrap()[0]; |
| 153 | let [first] = document.active(space).unwrap().nodes[&page].children[..] else { |
| 154 | panic!() |
| 155 | }; |
| 156 | let (add, insertion, _) = ops::new_outline(72.0, 144.0, "Second"); |
| 157 | let written = ops::page_op(&source, space, add).unwrap(); |
| 158 | let store = Store::parse(written.as_bytes()).unwrap(); |
| 159 | let index = RevisionIndex::parse(&store).unwrap(); |
| 160 | let document = Document::parse(&index).unwrap(); |
| 161 | let view = document.active(space).unwrap(); |
| 162 | for outline in [first, insertion] { |
| 163 | let Kind::Outline { indents } = &view.nodes[&outline].kind else { |
| 164 | panic!() |
| 165 | }; |
| 166 | assert_eq!(indents, &[18.0, 0.0, 27.0, 27.0]); |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | #[test] |
| 171 | fn a_created_title_outline_is_stored_and_read_as_onenote_stores_it() { |
| 172 | let source = onestore::create_section("pages.one", "Original", "Author").unwrap(); |
| 173 | let intent = PageCreation::new(None, Some("Title"), "Author").unwrap(); |
| 174 | let prepared = ops::section_op(&source, SectionOp::Create(intent.clone())).unwrap(); |
| 175 | let store = Store::parse(prepared.as_bytes()).unwrap(); |
| 176 | let index = RevisionIndex::parse(&store).unwrap(); |
| 177 | let document = Document::parse(&index).unwrap(); |
| 178 | let view = document.active(intent.space()).unwrap(); |
| 179 | let [title] = view.nodes[&intent.object()].structure[..] else { |
| 180 | panic!() |
| 181 | }; |
| 182 | let [outline] = view.nodes[&title].children[..] else { |
| 183 | panic!() |
| 184 | }; |
| 185 | let [paragraph] = view.nodes[&outline].children[..] else { |
| 186 | panic!() |
| 187 | }; |
| 188 | let Kind::Outline { indents } = &view.nodes[&outline].kind else { |
| 189 | panic!() |
| 190 | }; |
| 191 | assert_eq!(indents, &[18.0, 0.0, 27.0, 27.0]); |
| 192 | let flags = |id| -> Vec<u32> { |
| 193 | view.nodes[&id].extra[0] |
| 194 | .iter() |
| 195 | .map(|field| field.id) |
| 196 | .filter(|id| id >> 24 == 0x88) |
| 197 | .collect() |
| 198 | }; |
| 199 | assert_eq!( |
| 200 | flags(outline), |
| 201 | [0x88001cf9, 0x88001cb2, 0x88001cb4, 0x88001c91, 0x88001cff] |
| 202 | ); |
| 203 | assert_eq!(flags(paragraph), [0x88001cb2, 0x88001cb4]); |
| 204 | let page = Page::from_space(&document, intent.space()).unwrap(); |
| 205 | let [PageObject::Title(title)] = &page.objects[..] else { |
| 206 | panic!() |
| 207 | }; |
| 208 | let [outline] = &title.outlines[..] else { |
| 209 | panic!() |
| 210 | }; |
| 211 | assert!(outline.title); |
| 212 | assert_eq!(outline.indents, [18.0, 0.0, 27.0, 27.0]); |
| 213 | assert_eq!(outline.min_width, Some(162.0)); |
| 214 | assert_eq!(outline.layout.max_height, Some(21.6)); |
| 215 | } |
| 216 | |
| 217 | #[test] |
| 218 | fn invalid_page_intents_and_nonleading_anchors_reject_before_publication() { |
| 219 | for title in ["a\nb", "\r", "\0", "\u{fffc}", "\u{fddf}"] { |
| 220 | assert!(PageCreation::new(None, Some(title), "Author").is_err()); |
| 221 | } |
| 222 | assert!(PageCreation::new(None, None, "\0").is_err()); |
| 223 | assert!(PageCreation::new(Some(ExGuid::default()), None, "Author").is_err()); |
| 224 | let source = include_bytes!("../../../corpus/page-lifecycle/04-nested/notebook/Lifecycle.one"); |
| 225 | let store = Store::parse(source).unwrap(); |
| 226 | let index = RevisionIndex::parse(&store).unwrap(); |
| 227 | let pages = Document::parse(&index).unwrap().pages().unwrap(); |
| 228 | let intent = PageCreation::new(Some(pages[4].0), Some("Child"), "Author").unwrap(); |
| 229 | assert!(ops::section_op(source, SectionOp::Create(intent.clone())).is_err()); |
| 230 | let mut value = serde_json::to_value(PageCreation::new(None, None, "Author").unwrap()).unwrap(); |
| 231 | value["guid"] = serde_json::to_value([0_u8; 16]).unwrap(); |
| 232 | let invalid: PageCreation = serde_json::from_value(value.clone()).unwrap(); |
| 233 | assert!(ops::section_op(source, SectionOp::Create(invalid.clone())).is_err()); |
| 234 | value["unknown"] = true.into(); |
| 235 | assert!(serde_json::from_value::<PageCreation>(value).is_err()); |
| 236 | } |
| 237 | |
| 238 | #[test] |
| 239 | fn new_space_and_section_entry_publish_as_one_complete_action() { |
| 240 | let source = onestore::create_section("pages.one", "Original", "Author").unwrap(); |
| 241 | let intent = PageCreation::new(None, Some("New 🦋 é"), "Author").unwrap(); |
| 242 | let prepared = ops::section_op(&source, SectionOp::Create(intent.clone())).unwrap(); |
| 243 | let old = current::current(&source); |
| 244 | let new = current::current(prepared.as_bytes()); |
| 245 | assert_eq!(new.len(), old.len() + 1); |
| 246 | for write_limit in [1, 17, 4096] { |
| 247 | let mut complete = disk::Disk { |
| 248 | visible: source.clone(), |
| 249 | durable: source.clone(), |
| 250 | operation: 0, |
| 251 | fail_at: None, |
| 252 | write_limit, |
| 253 | random: 1954, |
| 254 | }; |
| 255 | prepared.commit(&mut complete).unwrap(); |
| 256 | assert_eq!(complete.durable, prepared.as_bytes()); |
| 257 | for fail_at in 1..=complete.operation { |
| 258 | let mut interrupted = disk::Disk { |
| 259 | visible: source.clone(), |
| 260 | durable: source.clone(), |
| 261 | operation: 0, |
| 262 | fail_at: Some(fail_at), |
| 263 | write_limit, |
| 264 | random: 1954 + fail_at as u64, |
| 265 | }; |
| 266 | prepared.commit(&mut interrupted).unwrap_err(); |
| 267 | let observed = current::current(&interrupted.durable); |
| 268 | assert!( |
| 269 | observed == old || observed == new, |
| 270 | "{write_limit}:{fail_at}" |
| 271 | ); |
| 272 | } |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | #[test] |
| 277 | fn repeated_page_creation_crosses_root_fragments_counters_and_section_checkpoints() { |
| 278 | let mut source = onestore::create_section("PageStress.one", "Original", "Author").unwrap(); |
| 279 | let original = source.clone(); |
| 280 | let original_store = Store::parse(&original).unwrap(); |
| 281 | let original_index = RevisionIndex::parse(&original_store).unwrap(); |
| 282 | let mut expected = Document::parse(&original_index).unwrap().pages().unwrap(); |
| 283 | let mut checkpoints = 0; |
| 284 | for step in 1..=520 { |
| 285 | let title = format!("Page {} 🦋 é", step % 7); |
| 286 | let intent = PageCreation::new( |
| 287 | None, |
| 288 | (step % 2 != 0).then_some(title.as_str()), |
| 289 | "Stress author", |
| 290 | ) |
| 291 | .unwrap(); |
| 292 | let prepared = ops::section_op(&source, SectionOp::Create(intent.clone())).unwrap(); |
| 293 | let mut unpublished = prepared.as_bytes().to_vec(); |
| 294 | unpublished[96..100].copy_from_slice(&source[96..100]); |
| 295 | let old_store = Store::parse(&unpublished).unwrap(); |
| 296 | let old_index = RevisionIndex::parse(&old_store).unwrap(); |
| 297 | assert_eq!( |
| 298 | Document::parse(&old_index).unwrap().pages().unwrap(), |
| 299 | expected |
| 300 | ); |
| 301 | let store = Store::parse(prepared.as_bytes()).unwrap(); |
| 302 | assert_eq!(store.header.root, original_store.header.root); |
| 303 | assert_eq!( |
| 304 | store.header.transaction_count, |
| 305 | original_store.header.transaction_count + step |
| 306 | ); |
| 307 | let index = RevisionIndex::parse(&store).unwrap(); |
| 308 | assert_eq!( |
| 309 | index.spaces.len(), |
| 310 | original_index.spaces.len() + step as usize |
| 311 | ); |
| 312 | expected.push((intent.space(), intent.object())); |
| 313 | assert_eq!(Document::parse(&index).unwrap().pages().unwrap(), expected); |
| 314 | let section = &index.spaces[&index.root]; |
| 315 | let rid = section.labels[&(ExGuid::default(), 1)]; |
| 316 | checkpoints += usize::from(section.revisions[&rid].dependency.is_none()); |
| 317 | for (sid, space) in &original_index.spaces { |
| 318 | for rid in space.revisions.keys() { |
| 319 | assert_eq!( |
| 320 | format!("{:?}", original_index.resolve(*sid, *rid).unwrap()), |
| 321 | format!("{:?}", index.resolve(*sid, *rid).unwrap()) |
| 322 | ); |
| 323 | } |
| 324 | } |
| 325 | source = prepared.as_bytes().to_vec(); |
| 326 | } |
| 327 | assert_eq!(checkpoints, 1); |
| 328 | if let Some(output) = std::env::var_os("ONESTORE_PAGE_STRESS_OUTPUT") { |
| 329 | std::fs::create_dir(&output).unwrap(); |
| 330 | std::fs::write( |
| 331 | std::path::Path::new(&output).join("PageStress.one"), |
| 332 | &source, |
| 333 | ) |
| 334 | .unwrap(); |
| 335 | } |
| 336 | } |
| 337 | |
| 338 | #[test] |
| 339 | fn native_changes_on_created_pages_accept_rust_followups() { |
| 340 | let initial = include_bytes!( |
| 341 | "../../../corpus/page-lifecycle/creation/native/cold/notebook/Lifecycle.one" |
| 342 | ); |
| 343 | let store = Store::parse(initial).unwrap(); |
| 344 | let index = RevisionIndex::parse(&store).unwrap(); |
| 345 | let document = Document::parse(&index).unwrap(); |
| 346 | let mut targets = Vec::new(); |
| 347 | for (sid, _) in document.pages().unwrap() { |
| 348 | let space = &document.spaces[&sid]; |
| 349 | let view = &space.revisions[&space.contexts[&ExGuid::default()]]; |
| 350 | let mut title = None; |
| 351 | let mut body = None; |
| 352 | for (id, node) in &view.nodes { |
| 353 | if let Kind::RichText { text, .. } = &node.kind { |
| 354 | if text == "New 🦋 é" { |
| 355 | title = Some(*id); |
| 356 | } |
| 357 | if text.starts_with("Native body ") { |
| 358 | body = Some(*id); |
| 359 | } |
| 360 | } |
| 361 | } |
| 362 | if let (Some(title), Some(body)) = (title, body) { |
| 363 | targets.push((sid, title, body)); |
| 364 | } |
| 365 | } |
| 366 | assert_eq!(targets.len(), 2); |
| 367 | let mut source = initial.to_vec(); |
| 368 | for (sid, title, body) in targets { |
| 369 | for (object, prefix) in [(body, "Rust + "), (title, "Reviewed ")] { |
| 370 | source = ops::page_op( |
| 371 | &source, |
| 372 | sid, |
| 373 | PageOp::Text { |
| 374 | text: object, |
| 375 | range: 0..0, |
| 376 | with: prefix.into(), |
| 377 | }, |
| 378 | ) |
| 379 | .unwrap() |
| 380 | .as_bytes() |
| 381 | .to_vec(); |
| 382 | } |
| 383 | let store = Store::parse(&source).unwrap(); |
| 384 | let index = RevisionIndex::parse(&store).unwrap(); |
| 385 | let document = Document::parse(&index).unwrap(); |
| 386 | let view = &document.spaces[&sid]; |
| 387 | let view = &view.revisions[&view.contexts[&ExGuid::default()]]; |
| 388 | assert!( |
| 389 | matches!(&view.nodes[&title].kind, Kind::RichText { text, .. } if text == "Reviewed New 🦋 é") |
| 390 | ); |
| 391 | assert!( |
| 392 | matches!(&view.nodes[&body].kind, Kind::RichText { text, .. } if text.starts_with("Rust + Native body ")) |
| 393 | ); |
| 394 | assert!( |
| 395 | matches!(&view.nodes[&view.roots[&2]].kind, Kind::Metadata { title: Some(title), .. } if title == "Reviewed New 🦋 é") |
| 396 | ); |
| 397 | } |
| 398 | if let Some(output) = std::env::var_os("ONESTORE_PAGE_FOLLOWUP_OUTPUT") { |
| 399 | std::fs::create_dir(&output).unwrap(); |
| 400 | std::fs::write(std::path::Path::new(&output).join("Lifecycle.one"), &source).unwrap(); |
| 401 | } |
| 402 | } |
| 403 | |
| 404 | #[test] |
| 405 | fn page_creation_preserves_native_features_and_file_data() { |
| 406 | for source in [ |
| 407 | include_bytes!("../../../corpus/native-ink/20260905-ui/notebook/synthetic.one").as_slice(), |
| 408 | include_bytes!("../../../corpus/native-external-assets/notebook/synthetic.one").as_slice(), |
| 409 | include_bytes!("../../../corpus/m6/native-features-01/notebook/Features.one").as_slice(), |
| 410 | ] { |
| 411 | let before = Store::parse(source).unwrap(); |
| 412 | let old = RevisionIndex::parse(&before).unwrap(); |
| 413 | let original = Document::parse(&old).unwrap(); |
| 414 | let intent = PageCreation::new(None, Some("Preserved features"), "Author").unwrap(); |
| 415 | let prepared = ops::section_op(source, SectionOp::Create(intent.clone())).unwrap(); |
| 416 | let after = Store::parse(prepared.as_bytes()).unwrap(); |
| 417 | let new = RevisionIndex::parse(&after).unwrap(); |
| 418 | let document = Document::parse(&new).unwrap(); |
| 419 | assert_eq!( |
| 420 | document.pages().unwrap().len(), |
| 421 | original.pages().unwrap().len() + 1 |
| 422 | ); |
| 423 | for (sid, space) in &old.spaces { |
| 424 | for rid in space.revisions.keys() { |
| 425 | let original = old.resolve(*sid, *rid).unwrap(); |
| 426 | let retained = new.resolve(*sid, *rid).unwrap(); |
| 427 | assert_eq!(format!("{original:?}"), format!("{retained:?}")); |
| 428 | for object in original.objects.values() { |
| 429 | if let Some(onestore::FileDataReference::Internal(id)) = |
| 430 | object.file_reference().unwrap() |
| 431 | { |
| 432 | assert_eq!(before.file_data(id).unwrap(), after.file_data(id).unwrap()); |
| 433 | } |
| 434 | } |
| 435 | } |
| 436 | if *sid != old.root { |
| 437 | assert_eq!(space.labels, new.spaces[sid].labels); |
| 438 | } |
| 439 | } |
| 440 | } |
| 441 | } |
| 442 | |
| 443 | /// A page created into an empty section as OneNote 2010 creates one (its title's date and |
| 444 | /// time fields, then a colour) reads back with both; a page kept from elsewhere keeps its |
| 445 | /// identity and creation time. |
| 446 | #[test] |
| 447 | fn dated_pages_in_an_empty_section_read_back_with_their_date_and_colour() { |
| 448 | let source = onestore::create_empty_section("New Section 1.one", Some(0x00e4a88a)).unwrap(); |
| 449 | let store = Store::parse(&source).unwrap(); |
| 450 | let index = RevisionIndex::parse(&store).unwrap(); |
| 451 | index.validate_current().unwrap(); |
| 452 | assert!(Document::parse(&index).unwrap().pages().unwrap().is_empty()); |
| 453 | let creation = PageCreation::new(None, Some(""), "Rust Author") |
| 454 | .unwrap() |
| 455 | .dated("Sunday, September 27, 2026", "2:23 AM") |
| 456 | .unwrap(); |
| 457 | let retained: PageCreation = |
| 458 | serde_json::from_slice(&serde_json::to_vec(&creation).unwrap()).unwrap(); |
| 459 | assert_eq!(retained, creation); |
| 460 | let space = creation.space(); |
| 461 | let identity = [7; 16]; |
| 462 | let kept = PageCreation::new(None, Some("Kept"), "Rust Author") |
| 463 | .unwrap() |
| 464 | .keeping(identity, 133_700_000_001_234_567) |
| 465 | .unwrap(); |
| 466 | let image = ops::edited( |
| 467 | &source, |
| 468 | vec![ |
| 469 | onestore::op::Op::Section(SectionOp::Create(creation.clone())), |
| 470 | onestore::op::Op::Page { |
| 471 | space, |
| 472 | op: PageOp::Color(Some(0x00f2f9d4)), |
| 473 | }, |
| 474 | onestore::op::Op::Section(SectionOp::Create(kept.clone())), |
| 475 | ], |
| 476 | ) |
| 477 | .unwrap(); |
| 478 | let store = Store::parse(&image).unwrap(); |
| 479 | let index = RevisionIndex::parse(&store).unwrap(); |
| 480 | index.validate_current().unwrap(); |
| 481 | let document = Document::parse(&index).unwrap(); |
| 482 | let page = Page::from_space(&document, space).unwrap(); |
| 483 | assert_eq!(page.color, Some(0x00f2f9d4)); |
| 484 | assert_eq!(page.created, Some(creation.created())); |
| 485 | let [PageObject::Title(title)] = &page.objects[..] else { |
| 486 | panic!("{:?}", page.objects) |
| 487 | }; |
| 488 | let shown: Vec<_> = title.outlines[1] |
| 489 | .paragraphs |
| 490 | .iter() |
| 491 | .map(|paragraph| { |
| 492 | let text = paragraph.text().unwrap(); |
| 493 | (text.date_field, text.text.text().to_owned()) |
| 494 | }) |
| 495 | .collect(); |
| 496 | assert_eq!(title.date, Some(title.outlines[1].id)); |
| 497 | assert_eq!( |
| 498 | shown, |
| 499 | [ |
| 500 | // OneNote's own new pages read the same: the fields' order names their roles. |
| 501 | (None, "Sunday, September 27, 2026".to_owned()), |
| 502 | (None, "2:23 AM".to_owned()), |
| 503 | ] |
| 504 | ); |
| 505 | let kept_page = Page::from_space(&document, kept.space()).unwrap(); |
| 506 | assert_eq!(kept_page.identity, Some(identity)); |
| 507 | assert_eq!(kept_page.created, Some(133_700_000_001_234_567)); |
| 508 | assert_eq!(kept_page.color, None); |
| 509 | // Clearing the colour stores OneNote's "No color". |
| 510 | let cleared = ops::page_edited(&image, space, vec![PageOp::Color(None)]).unwrap(); |
| 511 | let store = Store::parse(&cleared).unwrap(); |
| 512 | let index = RevisionIndex::parse(&store).unwrap(); |
| 513 | let document = Document::parse(&index).unwrap(); |
| 514 | assert_eq!(Page::from_space(&document, space).unwrap().color, None); |
| 515 | } |