Files
vectorcraft/crates/ui-egui/src/dialogs/tests_plugin_export.rs
T
Louise FoxandClaude Opus 5.5 2f893566ee Export As: a held Enter doesn't confirm the options; exporters get their file
Follow-ups to the review of the exporter plug-ins PR (#3171):

- Export As opens its format's options once the save panel has picked
  the file. The Enter that closed the panel, still held, confirmed
  them at once: Save As marks such a dialog (dialogs::AFTER_PANEL) so
  it waits for Enter to be let go, Export As didn't. Every options
  dialog it opens after the panel is marked now, an exporter plug-in's
  included, through one helper (dialogs::opened_by_panel) that Save As
  uses too.
- io::export with options that aren't a JSON object dropped the path
  and format it adds for the encoder: an exporter plug-in was told the
  document's name and its first extension instead of the file it
  writes. Such options now count as none.
- The exporter UI tests remove the plug-ins they install, so no other
  test's Export As lists them.

tests_export.rs reproduces the held Enter for every options dialog
(PNG, SVG, Save PDF, Text, DXF, EPS and an exporter's), and
tests_plugin_export.rs the options that aren't an object; both failed
before the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
2026-10-22 06:40:28 +07:00

137 lines
7.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Exporter plug-ins in File › Export › Export As: listed after the built-in formats, written
//! whole (Use Artboards is not theirs to apply), and their parameters asked for once the file is
//! picked. Each test installs exporters of its own and removes them: plug-ins are process-wide, and
//! a format left installed would show in other tests' Export As.
use serde_json::{Value, json};
use vectorcraft_plugins::wat::{self as tpl, Echo};
use super::tests_export::{app, app_saving, frame, kind, names, set};
use super::*;
fn install(app: &mut VectorcraftApp, src: &str) {
let wasm = wat::parse_str(src).unwrap();
app.run("plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&wasm)})).unwrap();
}
/// Export As lists an exporter after the built-in formats and writes its file of the whole
/// document (Use Artboards greyed out), with the exporter's warnings in the status bar.
#[test]
fn export_as_lists_exporters_and_writes_their_file_whole() {
let (mut app, written) = app(2);
install(&mut app, &tpl::returning(&tpl::exporter("test.ui.whole", r#"["tuw"]"#, "{}"), r#"{"dataBase64":"SGk=","warnings":["a note"]}"#));
// After the built-in formats, with the other exporters installed.
let listed: Vec<&str> = export_as::formats().iter().map(|f| f.id).collect();
let first = listed.iter().position(|id| id.starts_with("plugin.")).expect("an exporter listed");
let added = listed.get(first..).unwrap_or_default();
assert!(added.iter().all(|id| id.starts_with("plugin.")) && added.contains(&"plugin.test.ui.whole"), "{listed:?}");
// It isn't a filter: Object › Plug-ins' command says where it is.
let e = app.run("plugin.dialog", json!({"id": "test.ui.whole"})).unwrap_err();
assert!(e.contains("is an exporter (File › Export › Export As…)"), "{e}");
app.run("file.exportAs", json!({"format": "plugin.test.ui.whole"})).unwrap();
assert_eq!(kind(&app), Some("exportAs"));
assert_eq!(app.ui.dialog.as_ref().unwrap().str("format"), "plugin.test.ui.whole");
// Use Artboards is greyed out and doesn't apply: one file of the whole document.
set(&mut app, "useArtboards", json!(true));
frame(&mut app);
confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none(), "an exporter without parameters writes at once");
assert_eq!(names(&written), ["/out/Untitled-1.tuw"]);
assert_eq!(written.borrow()[0].1, b"Hi");
assert!(app.ui.status.contains("a note"), "{}", app.ui.status);
app.run("plugin.remove", json!({"id": "test.ui.whole"})).unwrap();
}
/// An exporter with parameters asks for them once the file is picked, and writes with them.
#[test]
fn an_exporter_with_parameters_asks_for_them_once_the_file_is_picked() {
let (mut app, written) = app(1);
let params = r#"{"density":{"type":"number","min":0,"max":10,"default":4}}"#;
install(&mut app, &tpl::echo_exporter(&tpl::exporter("test.ui.params", r#"["tup"]"#, params), Echo::Params));
// Asked for by its extension, as agents may.
app.run("file.exportAs", json!({"format": "tup"})).unwrap();
assert_eq!(app.ui.dialog.as_ref().unwrap().str("format"), "plugin.test.ui.params");
confirm(&mut app).unwrap();
assert_eq!(kind(&app), Some(plugin::EXPORT_KIND));
let d = app.ui.dialog.as_ref().unwrap();
assert_eq!((d.str("__label"), d.fields.get("density")), ("Test test.ui.params".to_string(), Some(&json!(4.0))));
assert!(written.borrow().is_empty(), "nothing written before Export");
frame(&mut app);
set(&mut app, "density", json!(7));
confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
assert_eq!(names(&written), ["/out/Untitled-1.tup"]);
let sent: Value = serde_json::from_slice(&written.borrow()[0].1).unwrap();
assert_eq!(sent, json!({"density": 7.0, "_context": {"mode": "export", "extension": "tup", "name": "Untitled-1"}}));
app.run("plugin.remove", json!({"id": "test.ui.params"})).unwrap();
}
/// `CRAFT_UI_PLUGIN_EXPORT_SHOT=<folder>` saves the pictures of the dialogs to that folder: Export As
/// with an exporter's format chosen, then the exporter's own dialog. The plug-in is a test one with
/// parameters a plotter's exporter could have, saved beside them (`plotter-test-exporter.wasm`) to
/// install in the app.
#[test]
fn export_as_and_exporter_dialog_pictures() {
let Some(dir) = std::env::var_os("CRAFT_UI_PLUGIN_EXPORT_SHOT") else { return };
let dir = std::path::PathBuf::from(dir);
let (mut app, _) = app(1);
let params = r#"{"speed":{"type":"number","min":1,"max":100,"default":40},"passes":{"type":"int","min":1,"max":10,"default":1},
"optimizeTravel":{"type":"bool","default":true},"paper":{"type":"choice","options":["A4","A3","Letter"],"default":"A4"}}"#;
let manifest = r#"{"id":"org.example.plotter","name":"Plotter (test exporter)","version":"1","kind":"exporter","extensions":["plt"],
"description":"Writes the document as pen plotter commands.","params":PARAMS}"#
.replace("PARAMS", params);
let src = tpl::echo_exporter(&manifest, Echo::Params);
std::fs::write(dir.join("plotter-test-exporter.wasm"), wat::parse_str(&src).unwrap()).unwrap();
install(&mut app, &src);
app.run("file.exportAs", json!({"format": "plugin.org.example.plotter"})).unwrap();
let shot = |app: VectorcraftApp, name: &str| -> VectorcraftApp {
let mut ready = false;
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(720.0, 520.0)).wgpu().build_ui_state(
move |ui, app: &mut VectorcraftApp| {
if !ready {
crate::theme::install_fonts(ui.ctx());
crate::theme::apply(ui.ctx(), crate::theme::Brightness::MediumDark);
ready = true;
return;
}
show(app, ui.ctx());
},
app,
);
h.run_steps(6);
h.render().unwrap().save(dir.join(name)).unwrap();
h.into_state()
};
app = shot(app, "export-as-exporter.png");
confirm(&mut app).unwrap();
assert_eq!(kind(&app), Some(plugin::EXPORT_KIND));
shot(app, "exporter-options.png");
}
/// A plug-in that declines keeps nothing written and says why.
#[test]
fn a_declining_exporter_writes_nothing() {
let (mut app, written) = app(1);
install(&mut app, &tpl::returning(&tpl::exporter("test.ui.declines", r#"["tud"]"#, "{}"), r#"{"error":"Nothing to plot."}"#));
app.run("file.exportAs", json!({"format": "plugin.test.ui.declines"})).unwrap();
let r = confirm(&mut app);
assert!(written.borrow().is_empty());
let said = r.err().unwrap_or_default() + &app.ui.status;
assert!(said.contains("Nothing to plot."), "{said}");
app.run("plugin.remove", json!({"id": "test.ui.declines"})).unwrap();
}
/// Export options that aren't an object are none: the exporter is still told the file it writes,
/// by its name and extension (here the second it lists), not the document's name and its first.
#[test]
fn an_exporter_is_told_its_file_whatever_the_options_are() {
let (mut app, written) = app_saving(1, |_| "/out/plotter-job.tnb".into());
install(&mut app, &tpl::echo_exporter(&tpl::exporter("test.ui.nulloptions", r#"["tna","tnb"]"#, "{}"), Echo::Params));
crate::io::export(&mut app, Some("plugin.test.ui.nulloptions"), None, &Value::Null).unwrap();
assert_eq!(names(&written), ["/out/plotter-job.tnb"]);
let sent: Value = serde_json::from_slice(&written.borrow()[0].1).unwrap();
assert_eq!(sent, json!({"_context": {"mode": "export", "extension": "tnb", "name": "plotter-job"}}));
app.run("plugin.remove", json!({"id": "test.ui.nulloptions"})).unwrap();
}