1#![cfg(feature = "gpu")]
2
3use canvas::{
4 gpu::{Paper, page::PageScene},
5 layout::TextEngine,
6};
7use onestore::{
8 Arena, ExGuid, PageCreation, RevisionIndex, Section, Store,
9 document::Document,
10 op::{Edit, Op, SectionOp},
11 page::{Page, PageObject, ParagraphContent},
12};
13use std::{fs, path::Path};
14
15fn pages(section: &[u8]) -> Vec<Page> {
16 let store = Store::parse(section).unwrap();
17 let index = RevisionIndex::parse(&store).unwrap();
18 let document = Document::parse(&index).unwrap();
19 spaces(&document)
20 .into_iter()
21 .map(|space| Page::from_space(&document, space).unwrap())
22 .collect()
23}
24
25fn spaces(document: &Document<'_>) -> Vec<ExGuid> {
26 let mut spaces: Vec<_> = document
27 .pages()
28 .unwrap()
29 .into_iter()
30 .map(|(space, _)| space)
31 .collect();
32 spaces.dedup();
33 spaces
34}
35
36/// What `notebook::session::Section::import_page` applies: a new titled page holding a copy of
37/// the editor's body objects.
38#[test]
39fn an_imported_editor_page_reopens_with_its_title_editable() {
40 let section = onestore::create_section("reopen.one", "Body 🦀 é", "Author").unwrap();
41 let mut engine = TextEngine::default();
42 let (_, editor) = PageScene::from_page(pages(&section).remove(0), &mut engine).unwrap();
43 let copy = editor.page().unwrap().copy().unwrap();
44 let creation = PageCreation::new(None, Some("Imported 🦋"), "Author").unwrap();
45 let arena = Arena::default();
46 let mut imported = Section::open(&arena, section).unwrap();
47 let import = SectionOp::Import {
48 creation,
49 page: copy,
50 };
51 let edit = Edit {
52 at: 134_000_000_000_000_000,
53 ops: vec![Op::Section(import)],
54 };
55 imported.apply("Author", &edit).unwrap();
56 imported.seal().unwrap();
57
58 let reopened = pages(&imported.image()).pop().unwrap();
59 let (_, editor) = PageScene::from_page(reopened.clone(), &mut engine).unwrap();
60 assert_eq!(editor.page().unwrap(), reopened);
61 let texts = |title: bool| -> Vec<String> {
62 editor
63 .outlines()
64 .iter()
65 .filter(|outline| outline.title == title)
66 .map(|outline| {
67 outline
68 .document()
69 .paragraphs()
70 .next()
71 .unwrap()
72 .text()
73 .to_string()
74 })
75 .collect()
76 };
77 assert_eq!(texts(true), ["Imported 🦋"]);
78 assert_eq!(texts(false), ["Body 🦀 é"]);
79}
80
81#[test]
82fn every_corpus_page_opens() {
83 fn sections(dir: &Path, found: &mut Vec<std::path::PathBuf>) {
84 for entry in fs::read_dir(dir).unwrap() {
85 let path = entry.unwrap().path();
86 if path.is_dir() {
87 sections(&path, found);
88 } else if path.extension().is_some_and(|extension| extension == "one") {
89 found.push(path);
90 }
91 }
92 }
93 let mut found = Vec::new();
94 sections(
95 &Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus"),
96 &mut found,
97 );
98 let mut engine = TextEngine::default();
99 let (mut pages, mut failures) = (0, Vec::new());
100 for path in found {
101 let bytes = fs::read(&path).unwrap();
102 // Malformed and encrypted sections are the reader's tests' concern.
103 let Ok(store) = Store::parse(&bytes) else {
104 continue;
105 };
106 let Ok(index) = RevisionIndex::parse(&store) else {
107 continue;
108 };
109 let Ok(document) = Document::parse(&index) else {
110 continue;
111 };
112 for space in spaces(&document) {
113 pages += 1;
114 let result = Page::from_space(&document, space)
115 .map_err(|error| format!("{error:?}"))
116 .and_then(|page| {
117 PageScene::from_page(page, &mut engine).map_err(|error| format!("{error:?}"))
118 });
119 if let Err(error) = result {
120 failures.push(format!("{} {space}: {error}", path.display()));
121 }
122 }
123 }
124 assert!(pages > 2000, "{pages}");
125 assert!(failures.is_empty(), "{}", failures.join("\n"));
126}
127
128/// A page opens whatever its pictures hold, and saving keeps their stored data.
129#[test]
130fn undecodable_pictures_show_placeholders_and_keep_their_data() {
131 let corpus = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus");
132 let section = |path: &str| pages(&fs::read(corpus.join(path)).unwrap());
133 let mut engine = TextEngine::default();
134
135 // A PNG whose checksum does not match, inside an outline.
136 let page = section("writer/complex-01/notebook/synthetic.one")
137 .into_iter()
138 .find(|page| page.title.starts_with("Fictitious"))
139 .unwrap();
140 let picture = page
141 .objects
142 .iter()
143 .find_map(|object| match object {
144 PageObject::Outline(outline) => {
145 outline
146 .paragraphs
147 .iter()
148 .find_map(|paragraph| match &paragraph.content {
149 ParagraphContent::Image(image) => Some(image.id),
150 _ => None,
151 })
152 }
153 _ => None,
154 })
155 .unwrap();
156 let (mut scene, editor) = PageScene::from_page(page.clone(), &mut engine).unwrap();
157 scene.settle(Some(&editor), 1.0, Paper::WHITE);
158 assert!(scene.image(picture).is_none());
159 assert_eq!(editor.page().unwrap(), page);
160
161 // A TIFF on the page renders; with its data damaged it becomes the placeholder.
162 let mut page = section("media-edit/candidate/Features.one")
163 .into_iter()
164 .find(|page| page.title == "Image tiff")
165 .unwrap();
166 let (mut scene, editor) = PageScene::from_page(page.clone(), &mut engine).unwrap();
167 scene.settle(Some(&editor), 1.0, Paper::WHITE);
168 let PageObject::Image(tiff) = page
169 .objects
170 .iter_mut()
171 .find(|object| matches!(object, PageObject::Image(_)))
172 .unwrap()
173 else {
174 unreachable!()
175 };
176 assert!(scene.image(tiff.id).is_some());
177 tiff.bytes = Some(b"not a picture".as_slice().into());
178 let (scene, editor) = PageScene::from_page(page.clone(), &mut engine).unwrap();
179 assert_eq!(
180 scene
181 .read_only(Some(&editor))
182 .map(|object| object.message)
183 .collect::<Vec<_>>(),
184 ["Image unavailable\nRead-only"]
185 );
186 assert_eq!(editor.page().unwrap(), page);
187}