Skip to main content

max / audiofiles

Cover the import flow's stages, not just its preflight The import flip landed with a fidelity test for the preflight and none for the flow, which is the half that matters: `/import` is one address whose answer depends on where the import has got to, so a test that visited one stage would say almost nothing about it. Four stages now -- idle, configuring, copying and stopped -- each asserting the description against what the host drew. Two reader gaps it found, both mine. A stand-in's way out was being dropped. `Node::StandIn` carries a sentence and an optional act, and the reader was treating the whole node as prose, so the idle screen's "Nothing is being imported" swallowed the Import... beside it. Two of goingson's twenty-seven stand-ins have one, which is why the member is optional and why it was easy to miss. And a required field is drawn with a marker after its label. `FieldStyle::required_marker` is how this renderer spells `Field::required`, so it normalizes the way the sort caret and the key hint already do: the same fact in the renderer's own spelling, not a difference.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-22 23:32 UTC
Signed with PGP, not checked
Commit: a2ad3f8027b2b446786cfc104b6927f3107a0b1a
Parent: fe964d6
1 file changed, +76 insertions, -0 deletions
@@ -356,6 +356,12 @@
356 356 }
357 357 }
358 358 }
359 + // A stand-in's way out. Two of goingson's twenty-seven have one, which
360 + // is why `act` is optional, and audiofiles' idle import screen is
361 + // another: "Nothing is being imported" with an Import... beside it. The
362 + // sentence is prose and the act is a control, so only the second is an
363 + // offer.
364 + Node::StandIn { act: Some(act), .. } => walk(&Node::Act(act.clone()), out),
359 365 // A token that calls a route is a control drawn as a chip -- the filter
360 366 // panel's twenty-four key pills are the site -- and one that calls
361 367 // nothing is a badge. `Tag::action` is the whole of the difference, so
@@ -525,6 +531,13 @@
525 531 said = stripped.trim_end();
526 532 }
527 533 }
534 + // A required field is drawn with a marker after its label. `Field::required`
535 + // is what the description says and `FieldStyle::required_marker` is how this
536 + // renderer shows it, so the asterisk is the same fact in the renderer's
537 + // spelling -- the caret's case again.
538 + if let Some(stripped) = said.strip_suffix('*') {
539 + said = stripped.trim_end();
540 + }
528 541 // An act carrying a key is drawn with it, as `label (key)`. The
529 542 // description says the key on the act instead, so this is the same
530 543 // normalization the caret gets: one fact, said two ways.
@@ -1121,3 +1134,66 @@
1121 1134 .in_a_window("Import folder")
1122 1135 .assert(&described, &drawn);
1123 1136 }
1137 +
1138 + /// The import flow's stages, in the states a test can stand one up in.
1139 + ///
1140 + /// The preflight has a test of its own because it is a modal rather than a
1141 + /// stage. This is the flow: one address whose answer depends on where the
1142 + /// import has got to, so a fidelity test that only visited one stage would say
1143 + /// almost nothing about it.
1144 + #[test]
1145 + fn the_import_flow_serves_what_it_describes_at_every_stage() {
1146 + type Reach = fn(&mut crate::state::BrowserState);
1147 +
1148 + let stages: [(&str, Reach); 4] = [
1149 + ("idle", |_state| {}),
1150 + ("configuring", |state| {
1151 + state.import_wf.import_mode = crate::state::ImportMode::ConfigureImport {
1152 + source: std::path::PathBuf::from("/music/kits"),
1153 + source_name: "kits".to_owned(),
1154 + strategy: crate::import::ImportStrategy::NewVfs {
1155 + vfs_name: "kits".to_owned(),
1156 + },
1157 + available_vfs: state.nav.vfs_list.to_vec(),
1158 + selected_merge_vfs_idx: 0,
1159 + new_vfs_name: "kits".to_owned(),
1160 + audio_file_count: 42,
1161 + };
1162 + }),
1163 + ("copying", |state| {
1164 + state.import_wf.import_mode = crate::state::ImportMode::Importing {
1165 + total: 42,
1166 + completed: 7,
1167 + current_name: "kick.wav".to_owned(),
1168 + walking: false,
1169 + walking_count: 0,
1170 + total_bytes: 9_000_000,
1171 + loose_files: false,
1172 + };
1173 + }),
1174 + ("stopped", |state| {
1175 + state.import_wf.import_mode = crate::state::ImportMode::OperationCancelled {
1176 + kind: crate::state::CancelKind::Import,
1177 + completed: 7,
1178 + total: 42,
1179 + destination: None,
1180 + };
1181 + }),
1182 + ];
1183 +
1184 + for (stage, reach) in stages {
1185 + let (mut state, _dir) = fixture();
1186 + reach(&mut state);
1187 +
1188 + let described = described(&super::panel::described_screen(&state, "/import"));
1189 + let drawn = shipped(|ui| {
1190 + super::panel::draw_import(ui, &mut state);
1191 + });
1192 +
1193 + described.addresses_resolve();
1194 + // A full-screen mode drawn into the app's own pane, so no frame to
1195 + // discount -- the same terms as the tag queue and the filter panel.
1196 + Parity::strict().assert(&described, &drawn);
1197 + println!(" {stage}: ok");
1198 + }
1199 + }