authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 13:49:32-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 14:23:18-07:00
log0fa3ee68b6f6521e11d14b69f59fbb546c6ad09e
tree62981ddac83304516cc784ed1d29702d3b47fad5
parent577710e1d47638bbd086a22366de263d858f4475
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: replay tests wait for the app to settle instead of fixed waits

`settle` and `accessibility` now also wait for loads, pages opening, work started with spawn (template art, version copies, renames) and the search index jobs sent but not answered, seen quiet on two frames apart so whatever a finishing thread sent has been handled. Search counts pending jobs in place of its busy flag, which was false until the index thread picked a job up. Every replay test now settles in place of its fixed waits, except after a resize, which the window system does. Assisted-by: claude-opus-5.5

4 files changed, 85 insertions(+), 55 deletions(-)

crates/snowbound/src/main.rs+33-11
...@@ -111,7 +111,11 @@ use std::{...@@ -111,7 +111,11 @@ use std::{
111 collections::{HashMap, HashSet},111 collections::{HashMap, HashSet},
112 error::Error,112 error::Error,
113 path::{Path, PathBuf},113 path::{Path, PathBuf},
114 sync::{Arc, Mutex, mpsc},114 sync::{
115 Arc, Mutex,
116 atomic::{AtomicUsize, Ordering},
117 mpsc,
118 },
115};119};
116use ui::{Axis, Flags, Id, Spec, Theme, Ui, children, fill, fit, px};120use ui::{Axis, Flags, Id, Spec, Theme, Ui, children, fill, fit, px};
117use web_time::Instant;121use web_time::Instant;
...@@ -886,7 +890,8 @@ struct State {...@@ -886,7 +890,8 @@ struct State {
886 snapshot: Option<PathBuf>,890 snapshot: Option<PathBuf>,
887 /// A replay waiting for nothing to be on its way, and where it wants the accessibility891 /// A replay waiting for nothing to be on its way, and where it wants the accessibility
888 /// tree written then.892 /// tree written then.
889 replay_settle: Option<(Option<PathBuf>, std::sync::mpsc::Sender<()>)>,893 /// Whether the last frame found the window quiet too, which the tree waits for.
894 replay_settle: Option<(Option<PathBuf>, std::sync::mpsc::Sender<()>, bool)>,
890 /// `SNOWBOUND_FRAMES`: a directory every frame drawn is also written to, named by895 /// `SNOWBOUND_FRAMES`: a directory every frame drawn is also written to, named by
891 /// milliseconds since the window opened.896 /// milliseconds since the window opened.
892 frames: Option<(PathBuf, Instant)>,897 frames: Option<(PathBuf, Instant)>,
...@@ -5629,7 +5634,15 @@ fn page_text(editor: &CanvasEditor) -> String {...@@ -5629,7 +5634,15 @@ fn page_text(editor: &CanvasEditor) -> String {
56295634
5630/// Runs `work` beside the frame: on a thread of its own, or in the browser, which gives the5635/// Runs `work` beside the frame: on a thread of its own, or in the browser, which gives the
5631/// page one thread, once the frame under way is done.5636/// page one thread, once the frame under way is done.
5637/// Work [`spawn`] started that has not ended.
5638static RUNNING: AtomicUsize = AtomicUsize::new(0);
5639
5632fn spawn(work: impl FnOnce() + Send + 'static) {5640fn spawn(work: impl FnOnce() + Send + 'static) {
5641 RUNNING.fetch_add(1, Ordering::Relaxed);
5642 let work = move || {
5643 work();
5644 RUNNING.fetch_sub(1, Ordering::Relaxed);
5645 };
5633 #[cfg(not(target_arch = "wasm32"))]5646 #[cfg(not(target_arch = "wasm32"))]
5634 std::thread::spawn(work);5647 std::thread::spawn(work);
5635 #[cfg(target_arch = "wasm32")]5648 #[cfg(target_arch = "wasm32")]
...@@ -5714,7 +5727,9 @@ impl State {...@@ -5714,7 +5727,9 @@ impl State {
5714 }5727 }
5715 }5728 }
5716 Replay::Snapshot(path) => self.snapshot = Some(path),5729 Replay::Snapshot(path) => self.snapshot = Some(path),
5717 Replay::Settle(path, settled) => self.replay_settle = Some((path, settled)),5730 Replay::Settle(path, settled) => {
5731 self.replay_settle = Some((path, settled, false));
5732 }
5718 Replay::Tick | Replay::Quit => {}5733 Replay::Tick | Replay::Quit => {}
5719 Replay::Appearance(appearance) => {5734 Replay::Appearance(appearance) => {
5720 self.window.set_theme(Some(appearance));5735 self.window.set_theme(Some(appearance));
...@@ -5730,15 +5745,22 @@ impl State {...@@ -5730,15 +5745,22 @@ impl State {
5730 if let Err(error) = self.frame() {5745 if let Err(error) = self.frame() {
5731 eprintln!("{error}");5746 eprintln!("{error}");
5732 }5747 }
5733 if self.settled()5748 // Quiet on two frames apart, what a thread sent as it ended has been taken.
5734 && let Some((path, settled)) = self.replay_settle.take()5749 let quiet = self.settled()
5735 {5750 && self.opening.is_none()
5736 if let Some(path) = path5751 && RUNNING.load(Ordering::Relaxed) == 0
5737 && let Err(error) = self.write_accessibility(&path)5752 && self.search.pending.load(Ordering::Relaxed) == 0;
5738 {5753 if let Some((path, settled, was)) = self.replay_settle.take() {
5739 eprintln!("{error}");5754 if !(quiet && was) {
5755 self.replay_settle = Some((path, settled, quiet));
5756 } else {
5757 if let Some(path) = path
5758 && let Err(error) = self.write_accessibility(&path)
5759 {
5760 eprintln!("{error}");
5761 }
5762 let _ = settled.send(());
5740 }5763 }
5741 let _ = settled.send(());
5742 }5764 }
5743 }5765 }
5744 #[cfg(not(target_arch = "wasm32"))]5766 #[cfg(not(target_arch = "wasm32"))]
crates/snowbound/src/pane.rs+2-2
...@@ -754,7 +754,7 @@ impl State {...@@ -754,7 +754,7 @@ impl State {
754 if found.is_empty() {754 if found.is_empty() {
755 let status = if self.search.query.trim().is_empty() {755 let status = if self.search.query.trim().is_empty() {
756 ""756 ""
757 } else if self.search.busy.load(Ordering::Relaxed) {757 } else if self.search.pending.load(Ordering::Relaxed) > 0 {
758 "Searching…"758 "Searching…"
759 } else {759 } else {
760 "No matches"760 "No matches"
...@@ -837,7 +837,7 @@ impl State {...@@ -837,7 +837,7 @@ impl State {
837 mut unchecked: bool,837 mut unchecked: bool,
838 mut scope: TagScope,838 mut scope: TagScope,
839 ) {839 ) {
840 let status = if self.search.busy.load(Ordering::Relaxed) {840 let status = if self.search.pending.load(Ordering::Relaxed) > 0 {
841 "Searching…"841 "Searching…"
842 } else {842 } else {
843 "Search completed"843 "Search completed"
crates/snowbound/src/search.rs+17-11
...@@ -14,7 +14,7 @@ use std::{...@@ -14,7 +14,7 @@ use std::{
14 path::{Path, PathBuf},14 path::{Path, PathBuf},
15 sync::{15 sync::{
16 Arc, Mutex, Weak,16 Arc, Mutex, Weak,
17 atomic::{AtomicBool, AtomicU64, Ordering},17 atomic::{AtomicU64, AtomicUsize, Ordering},
18 mpsc,18 mpsc,
19 },19 },
20 time::{Duration, SystemTime},20 time::{Duration, SystemTime},
...@@ -115,7 +115,8 @@ pub struct Search {...@@ -115,7 +115,8 @@ pub struct Search {
115 pub(crate) index: Arc<Mutex<Index>>,115 pub(crate) index: Arc<Mutex<Index>>,
116 /// Counts the index's changes, so results are made again when it changes.116 /// Counts the index's changes, so results are made again when it changes.
117 version: Arc<AtomicU64>,117 version: Arc<AtomicU64>,
118 pub(crate) busy: Arc<AtomicBool>,118 /// Jobs sent the index and not yet answered.
119 pub(crate) pending: Arc<AtomicUsize>,
119 jobs: mpsc::Sender<Job>,120 jobs: mpsc::Sender<Job>,
120 /// The index's own state, which the browser, having no thread for it, keeps here to run121 /// The index's own state, which the browser, having no thread for it, keeps here to run
121 /// the jobs sent after each frame.122 /// the jobs sent after each frame.
...@@ -140,10 +141,14 @@ impl Search {...@@ -140,10 +141,14 @@ impl Search {
140 let (jobs, receiver) = mpsc::channel();141 let (jobs, receiver) = mpsc::channel();
141 let index = Arc::new(Mutex::new(Index::default()));142 let index = Arc::new(Mutex::new(Index::default()));
142 let version = Arc::new(AtomicU64::new(0));143 let version = Arc::new(AtomicU64::new(0));
143 let busy = Arc::new(AtomicBool::new(false));144 let pending = Arc::new(AtomicUsize::new(0));
144 let indexing = Indexing {145 let indexing = Indexing {
145 jobs: receiver,146 jobs: receiver,
146 shared: (Arc::clone(&index), Arc::clone(&version), Arc::clone(&busy)),147 shared: (
148 Arc::clone(&index),
149 Arc::clone(&version),
150 Arc::clone(&pending),
151 ),
147 redraw,152 redraw,
148 stamps: HashMap::new(),153 stamps: HashMap::new(),
149 };154 };
...@@ -163,7 +168,7 @@ impl Search {...@@ -163,7 +168,7 @@ impl Search {
163 reveal: None,168 reveal: None,
164 index,169 index,
165 version,170 version,
166 busy,171 pending,
167 jobs,172 jobs,
168 #[cfg(target_arch = "wasm32")]173 #[cfg(target_arch = "wasm32")]
169 indexing,174 indexing,
...@@ -189,6 +194,7 @@ impl Search {...@@ -189,6 +194,7 @@ impl Search {
189 }194 }
190195
191 fn send(&self, job: Job) {196 fn send(&self, job: Job) {
197 self.pending.fetch_add(1, Ordering::Relaxed);
192 let _ = self.jobs.send(job);198 let _ = self.jobs.send(job);
193 #[cfg(target_arch = "wasm32")]199 #[cfg(target_arch = "wasm32")]
194 {200 {
...@@ -239,7 +245,7 @@ pub(crate) fn now() -> u64 {...@@ -239,7 +245,7 @@ pub(crate) fn now() -> u64 {
239/// The index thread's jobs and what it keeps between them.245/// The index thread's jobs and what it keeps between them.
240struct Indexing {246struct Indexing {
241 jobs: mpsc::Receiver<Job>,247 jobs: mpsc::Receiver<Job>,
242 shared: (Arc<Mutex<Index>>, Arc<AtomicU64>, Arc<AtomicBool>),248 shared: (Arc<Mutex<Index>>, Arc<AtomicU64>, Arc<AtomicUsize>),
243 redraw: std::task::Waker,249 redraw: std::task::Waker,
244 stamps: HashMap<String, Stamp>,250 stamps: HashMap<String, Stamp>,
245}251}
...@@ -249,7 +255,6 @@ impl Indexing {...@@ -249,7 +255,6 @@ impl Indexing {
249 #[cfg(not(target_arch = "wasm32"))]255 #[cfg(not(target_arch = "wasm32"))]
250 fn run(mut self) {256 fn run(mut self) {
251 while let Ok(first) = self.jobs.recv() {257 while let Ok(first) = self.jobs.recv() {
252 self.shared.2.store(true, Ordering::Relaxed);
253 // Typing sends a job a keystroke; a pause gathers them into one read.258 // Typing sends a job a keystroke; a pause gathers them into one read.
254 if matches!(first, Job::Pages { .. }) {259 if matches!(first, Job::Pages { .. }) {
255 std::thread::sleep(SETTLE);260 std::thread::sleep(SETTLE);
...@@ -261,13 +266,14 @@ impl Indexing {...@@ -261,13 +266,14 @@ impl Indexing {
261 /// Answers `first` and the jobs waiting behind it, the newest notebooks first, then page266 /// Answers `first` and the jobs waiting behind it, the newest notebooks first, then page
262 /// changes, and wakes the window once the index changed.267 /// changes, and wakes the window once the index changed.
263 fn answer(&mut self, first: Job) {268 fn answer(&mut self, first: Job) {
264 let (index, version, busy) = &self.shared;269 let (index, version, pending) = &self.shared;
265 let (jobs, stamps) = (&self.jobs, &mut self.stamps);270 let (jobs, stamps) = (&self.jobs, &mut self.stamps);
266 busy.store(true, Ordering::Relaxed);
267 let mut notebooks = None;271 let mut notebooks = None;
268 let mut changed = HashSet::new();272 let mut changed = HashSet::new();
269 let mut pages: HashMap<String, (Weak<Replica>, HashSet<ExGuid>)> = HashMap::new();273 let mut pages: HashMap<String, (Weak<Replica>, HashSet<ExGuid>)> = HashMap::new();
274 let mut answered = 0;
270 for job in std::iter::once(first).chain(jobs.try_iter()) {275 for job in std::iter::once(first).chain(jobs.try_iter()) {
276 answered += 1;
271 match job {277 match job {
272 Job::Notebooks {278 Job::Notebooks {
273 libraries,279 libraries,
...@@ -300,7 +306,7 @@ impl Indexing {...@@ -300,7 +306,7 @@ impl Indexing {
300 version.fetch_add(1, Ordering::Relaxed);306 version.fetch_add(1, Ordering::Relaxed);
301 }307 }
302 lap("index", start);308 lap("index", start);
303 busy.store(false, Ordering::Relaxed);309 pending.fetch_sub(answered, Ordering::Relaxed);
304 self.redraw.wake_by_ref();310 self.redraw.wake_by_ref();
305 }311 }
306}312}
...@@ -933,7 +939,7 @@ impl State {...@@ -933,7 +939,7 @@ impl State {
933 // OneNote's status line: whether the search is done, and where it looked.939 // OneNote's status line: whether the search is done, and where it looked.
934 let status = if query.is_empty() {940 let status = if query.is_empty() {
935 "Search In:"941 "Search In:"
936 } else if self.search.busy.load(Ordering::Relaxed) {942 } else if self.search.pending.load(Ordering::Relaxed) > 0 {
937 "Searching:"943 "Searching:"
938 } else if found.is_empty() {944 } else if found.is_empty() {
939 "No matches:"945 "No matches:"
crates/snowbound/tests/replay.rs+33-31
...@@ -38,13 +38,13 @@ fn replay(scratch: &Scratch, notebook: Option<&Path>, steps: &[&str]) -> Vec<Str...@@ -38,13 +38,13 @@ fn replay(scratch: &Scratch, notebook: Option<&Path>, steps: &[&str]) -> Vec<Str
38 std::fs::copy(entry.path(), dir.join("notebook").join(entry.file_name())).unwrap();38 std::fs::copy(entry.path(), dir.join("notebook").join(entry.file_name())).unwrap();
39 }39 }
40 }40 }
41 let mut script = String::from("wait 500\n");41 let mut script = String::from("settle\n");
42 let mut trees = Vec::new();42 let mut trees = Vec::new();
43 for step in steps {43 for step in steps {
44 match step.strip_prefix("accessibility ") {44 match step.strip_prefix("accessibility ") {
45 Some(name) => {45 Some(name) => {
46 let path = dir.join(format!("{name}.txt"));46 let path = dir.join(format!("{name}.txt"));
47 script += &format!("accessibility {}\nwait 100\n", path.display());47 script += &format!("accessibility {}\n", path.display());
48 trees.push(path);48 trees.push(path);
49 }49 }
50 None => script += &format!("{step}\n"),50 None => script += &format!("{step}\n"),
...@@ -115,6 +115,7 @@ fn each_toolbar_control_has_a_name_of_its_own_at_every_width() {...@@ -115,6 +115,7 @@ fn each_toolbar_control_has_a_name_of_its_own_at_every_width() {
115 let mut steps = Vec::new();115 let mut steps = Vec::new();
116 for width in ["1600", "1180", "900", "600"] {116 for width in ["1600", "1180", "900", "600"] {
117 steps.push(format!("resize {width} 760"));117 steps.push(format!("resize {width} 760"));
118 // The window system resizes the window, which settling does not wait for.
118 steps.push("wait 300".into());119 steps.push("wait 300".into());
119 steps.push(format!("accessibility {width}"));120 steps.push(format!("accessibility {width}"));
120 }121 }
...@@ -140,17 +141,17 @@ fn the_palette_lists_recent_pages_and_the_actions_a_context_menu_offers() {...@@ -140,17 +141,17 @@ fn the_palette_lists_recent_pages_and_the_actions_a_context_menu_offers() {
140 let scratch = Scratch::new("palette-actions");141 let scratch = Scratch::new("palette-actions");
141 let notebook =142 let notebook =
142 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate");143 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate");
143 let palette = ["modifiers command", "key p", "modifiers", "wait 400"];144 let palette = ["modifiers command", "key p", "modifiers", "settle"];
144 let chord = ["modifiers command", "key k", "modifiers", "wait 400"];145 let chord = ["modifiers command", "key k", "modifiers", "settle"];
145 let mut steps = Vec::from(palette);146 let mut steps = Vec::from(palette);
146 steps.extend(["type type over", "wait 200", "key Enter", "wait 800"]);147 steps.extend(["type type over", "settle", "key Enter", "settle"]);
147 steps.extend(palette);148 steps.extend(palette);
148 steps.push("accessibility recent");149 steps.push("accessibility recent");
149 steps.extend(chord);150 steps.extend(chord);
150 steps.push("accessibility actions");151 steps.push("accessibility actions");
151 steps.extend(["key Escape", "wait 400", "accessibility back"]);152 steps.extend(["key Escape", "accessibility back"]);
152 steps.extend(["key Escape", "wait 400", "move 1000 87", "press right"]);153 steps.extend(["key Escape", "settle", "move 1000 87", "press right"]);
153 steps.extend(["release right", "wait 400", "accessibility context"]);154 steps.extend(["release right", "accessibility context"]);
154 let [recent, actions, back, context] = replay(&scratch, Some(&notebook), &steps)155 let [recent, actions, back, context] = replay(&scratch, Some(&notebook), &steps)
155 .try_into()156 .try_into()
156 .unwrap();157 .unwrap();
...@@ -201,11 +202,11 @@ fn undo_walks_back_through_new_pages_and_their_titles() {...@@ -201,11 +202,11 @@ fn undo_walks_back_through_new_pages_and_their_titles() {
201 let notebook =202 let notebook =
202 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate");203 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate");
203 let chord =204 let chord =
204 |modifiers: &str, key: &str| [modifiers, key, "modifiers", "wait 800"].map(String::from);205 |modifiers: &str, key: &str| [modifiers, key, "modifiers", "settle"].map(String::from);
205 let mut steps = Vec::new();206 let mut steps = Vec::new();
206 for title in ["type One", "type Two"] {207 for title in ["type One", "type Two"] {
207 steps.extend(chord("modifiers command", "key n"));208 steps.extend(chord("modifiers command", "key n"));
208 steps.extend(["settle", title, "wait 300"].map(String::from));209 steps.push(title.into());
209 }210 }
210 steps.extend(chord("modifiers command control", "key Left"));211 steps.extend(chord("modifiers command control", "key Left"));
211 steps.push("accessibility back".into());212 steps.push("accessibility back".into());
...@@ -267,11 +268,7 @@ fn search(query: &str, name: &str) -> Vec<String> {...@@ -267,11 +268,7 @@ fn search(query: &str, name: &str) -> Vec<String> {
267 ["modifiers command", "key e", "modifiers"]268 ["modifiers command", "key e", "modifiers"]
268 .into_iter()269 .into_iter()
269 .map(String::from)270 .map(String::from)
270 .chain([271 .chain([format!("type {query}"), format!("accessibility {name}")])
271 format!("type {query}"),
272 "wait 2500".into(),
273 format!("accessibility {name}"),
274 ])
275 .collect()272 .collect()
276}273}
277274
...@@ -279,13 +276,13 @@ fn search(query: &str, name: &str) -> Vec<String> {...@@ -279,13 +276,13 @@ fn search(query: &str, name: &str) -> Vec<String> {
279fn version_menu() -> Vec<String> {276fn version_menu() -> Vec<String> {
280 let mut steps: Vec<String> = ["modifiers command shift", "key p", "modifiers"]277 let mut steps: Vec<String> = ["modifiers command shift", "key p", "modifiers"]
281 .into_iter()278 .into_iter()
282 .chain(["type Page Versions", "wait 300", "key Enter", "wait 1000"])279 .chain(["type Page Versions", "settle", "key Enter", "settle"])
283 .map(String::from)280 .map(String::from)
284 .collect();281 .collect();
285 // The version row under the page, then the yellow bar above the version.282 // The version row under the page, then the yellow bar above the version.
286 for point in ["1003 114", "177 94"] {283 for point in ["1003 114", "177 94"] {
287 steps.extend([format!("move {point}"), "press".into(), "release".into()]);284 steps.extend([format!("move {point}"), "press".into(), "release".into()]);
288 steps.push("wait 1200".into());285 steps.push("settle".into());
289 }286 }
290 steps287 steps
291}288}
...@@ -301,10 +298,10 @@ fn search_finds_what_a_template_put_on_the_page() {...@@ -301,10 +298,10 @@ fn search_finds_what_a_template_put_on_the_page() {
301 let scratch = Scratch::new("template-search");298 let scratch = Scratch::new("template-search");
302 let notebook =299 let notebook =
303 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate");300 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate");
304 let mut steps: Vec<String> = ["modifiers command", "key n", "modifiers", "wait 1200"]301 let mut steps: Vec<String> = ["modifiers command", "key n", "modifiers", "settle"]
305 .into_iter()302 .into_iter()
306 // The Meeting tile of the template strip over the new page.303 // The Meeting tile of the template strip over the new page.
307 .chain(["move 615 287", "press", "release", "wait 3000"])304 .chain(["move 615 287", "press", "release", "settle"])
308 .map(String::from)305 .map(String::from)
309 .collect();306 .collect();
310 steps.extend(search("Attendees", "found"));307 steps.extend(search("Attendees", "found"));
...@@ -325,7 +322,7 @@ fn search_finds_a_restored_version() {...@@ -325,7 +322,7 @@ fn search_finds_a_restored_version() {
325 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/page-versions/candidate-restore");322 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/page-versions/candidate-restore");
326 let mut steps = version_menu();323 let mut steps = version_menu();
327 // Restore Version.324 // Restore Version.
328 steps.extend(["key Down", "key Enter", "wait 2000"].map(String::from));325 steps.extend(["key Down", "key Enter", "settle"].map(String::from));
329 steps.extend(search("Second author", "found"));326 steps.extend(search("Second author", "found"));
330 let [found] = run(&scratch, &notebook, &steps).try_into().unwrap();327 let [found] = run(&scratch, &notebook, &steps).try_into().unwrap();
331 assert!(!search_results(&found).is_empty(), "{found}");328 assert!(!search_results(&found).is_empty(), "{found}");
...@@ -339,8 +336,8 @@ fn a_version_copied_into_its_section_is_listed_and_found() {...@@ -339,8 +336,8 @@ fn a_version_copied_into_its_section_is_listed_and_found() {
339 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/page-versions/candidate-restore");336 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/page-versions/candidate-restore");
340 let mut steps = version_menu();337 let mut steps = version_menu();
341 // Copy Page To…, then the section itself.338 // Copy Page To…, then the section itself.
342 steps.extend(["key Down", "key Down", "key Down", "key Enter", "wait 600"].map(String::from));339 steps.extend(["key Down", "key Down", "key Down", "key Enter", "settle"].map(String::from));
343 steps.extend(["key Down", "key Enter", "wait 2000", "accessibility copied"].map(String::from));340 steps.extend(["key Down", "key Enter", "accessibility copied"].map(String::from));
344 steps.extend(search("Second author", "found"));341 steps.extend(search("Second author", "found"));
345 let [copied, found] = run(&scratch, &notebook, &steps).try_into().unwrap();342 let [copied, found] = run(&scratch, &notebook, &steps).try_into().unwrap();
346 let pages = page_tabs(&copied);343 let pages = page_tabs(&copied);
...@@ -359,9 +356,9 @@ fn renaming_a_section_from_its_tab_types_into_the_sidebar_at_once() {...@@ -359,9 +356,9 @@ fn renaming_a_section_from_its_tab_types_into_the_sidebar_at_once() {
359 let scratch = Scratch::new("rename-shut-sidebar");356 let scratch = Scratch::new("rename-shut-sidebar");
360 let notebook =357 let notebook =
361 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate");358 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate");
362 let mut steps = vec!["move 53 53", "press right", "release right", "wait 400"];359 let mut steps = vec!["move 53 53", "press right", "release right", "settle"];
363 steps.extend(["key Down", "key Enter", "wait 400"]);360 steps.extend(["key Down", "key Enter", "settle"]);
364 steps.extend(["type Renamed", "wait 100", "accessibility typed"]);361 steps.extend(["type Renamed", "accessibility typed"]);
365 let [typed] = replay(&scratch, Some(&notebook), &steps)362 let [typed] = replay(&scratch, Some(&notebook), &steps)
366 .try_into()363 .try_into()
367 .unwrap();364 .unwrap();
...@@ -378,10 +375,10 @@ fn the_find_bar_keeps_room_for_the_query() {...@@ -378,10 +375,10 @@ fn the_find_bar_keeps_room_for_the_query() {
378 let scratch = Scratch::new("find-room");375 let scratch = Scratch::new("find-room");
379 let notebook =376 let notebook =
380 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate");377 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate");
381 let mut steps = vec!["modifiers command", "key f", "modifiers", "wait 200"];378 let mut steps = vec!["modifiers command", "key f", "modifiers", "settle"];
382 steps.extend(["type Alpha", "wait 400", "move 600 600", "press", "release"]);379 steps.extend(["type Alpha", "settle", "move 600 600", "press", "release"]);
383 steps.extend(["wait 200", "move 1000 52", "press", "release", "wait 200"]);380 steps.extend(["settle", "move 1000 52", "press", "release", "settle"]);
384 steps.extend(["type zz", "wait 200", "accessibility found"]);381 steps.extend(["type zz", "accessibility found"]);
385 let [found] = replay(&scratch, Some(&notebook), &steps)382 let [found] = replay(&scratch, Some(&notebook), &steps)
386 .try_into()383 .try_into()
387 .unwrap();384 .unwrap();
...@@ -417,8 +414,13 @@ fn a_launch_returns_to_the_page_each_notebook_was_left_on() {...@@ -417,8 +414,13 @@ fn a_launch_returns_to_the_page_each_notebook_was_left_on() {
417 });414 });
418 std::fs::write(scratch.0.join("settings.json"), settings.to_string()).unwrap();415 std::fs::write(scratch.0.join("settings.json"), settings.to_string()).unwrap();
419 // The other notebook's section in the sidebar.416 // The other notebook's section in the sidebar.
420 let mut steps = vec!["accessibility launched", "move 63 104", "press", "release"];417 let steps = [
421 steps.extend(["wait 4000", "accessibility other"]);418 "accessibility launched",
419 "move 63 104",
420 "press",
421 "release",
422 "accessibility other",
423 ];
422 let shown = |tree: &str| page_tabs(tree).into_iter().find(|tab| tab.starts_with('*'));424 let shown = |tree: &str| page_tabs(tree).into_iter().find(|tab| tab.starts_with('*'));
423 let [launched, other] = replay(&scratch, Some(&source), &steps).try_into().unwrap();425 let [launched, other] = replay(&scratch, Some(&source), &steps).try_into().unwrap();
424 assert_eq!(426 assert_eq!(