mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
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
137 lines
7.5 KiB
Rust
137 lines
7.5 KiB
Rust
//! 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();
|
||
}
|