Merge pull request #3979 from dotCooCoo/fix/remembered-artboard-pick

Save: a remembered artboard pick follows the artboards it named
This commit is contained in:
BFlat
2026-10-24 17:51:44 +07:00
committed by GitHub
10 changed files with 317 additions and 9 deletions
+40 -1
View File
@@ -959,6 +959,12 @@ pub struct Document {
/// engine sets it from the selection's ([`Selection::new_art`]). Editing state, never saved.
#[serde(skip)]
pub new_art: NewArtIds,
/// The highest artboard id given out ([`Document::next_artboard_id`]) or deleted from the
/// document ([`Document::retire_artboard_id`]): a new artboard takes a higher one. Shared by
/// every copy of the document, the undo history's included, so an undo does not lower it.
/// Never saved.
#[serde(skip)]
pub retired_artboard_id: RetiredArtboardId,
/// Some object is a dimension (the Dimension tool): edits refresh dimensions only while this
/// is set, so documents without any pay nothing for them.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
@@ -968,6 +974,27 @@ pub struct Document {
index: id_index::IdIndex,
}
/// [`Document::retired_artboard_id`]: not document content, so two documents are equal whatever it
/// holds. A clone shares the value with the document it was cloned from.
#[derive(Clone, Debug, Default)]
pub struct RetiredArtboardId(Arc<std::sync::atomic::AtomicU32>);
impl RetiredArtboardId {
pub fn get(&self) -> u32 {
self.0.load(std::sync::atomic::Ordering::Relaxed)
}
/// Raise the value to `id` when `id` is higher.
pub fn raise(&self, id: u32) {
self.0.fetch_max(id, std::sync::atomic::Ordering::Relaxed);
}
}
impl PartialEq for RetiredArtboardId {
fn eq(&self, _: &Self) -> bool {
true
}
}
fn ppi72() -> f64 {
72.0
}
@@ -1052,6 +1079,7 @@ impl Document {
puppet,
new_art,
dimensions,
retired_artboard_id: _,
index: _,
} = self;
*version == other.version
@@ -1225,6 +1253,7 @@ impl Document {
puppet: None,
new_art: Default::default(),
dimensions: false,
retired_artboard_id: Default::default(),
index: Default::default(),
};
let id = d.alloc_id();
@@ -1940,8 +1969,18 @@ impl Document {
self.guides.iter().filter(|g| g.artboard == Some(from)).map(|g| Guide { artboard: Some(to), ..g.moved(d) }).collect();
self.guides.extend(copies);
}
/// An id for a new artboard: one no artboard has, above the ids given out before and the ids of
/// the artboards deleted from the document ([`Document::retired_artboard_id`]). The id counts
/// as given out, so an artboard made after an undo does not take the id of one the undo took
/// away.
pub fn next_artboard_id(&self) -> u32 {
free_id(self.artboards.iter().map(|a| a.id))
let id = free_id(self.artboards.iter().map(|a| a.id).chain([self.retired_artboard_id.get()]));
self.retired_artboard_id.raise(id);
id
}
/// Record that the artboard with `id` was deleted: no later artboard takes its id.
pub fn retire_artboard_id(&self, id: u32) {
self.retired_artboard_id.raise(id);
}
pub fn pattern(&self, name: &str) -> Option<&PatternDef> {
self.patterns.iter().find(|p| p.name == name)
+2 -2
View File
@@ -74,8 +74,8 @@ pub use native::with_compression_pref;
pub use pdfimport::{LoadOptions, page_document};
pub(crate) use save::job_for;
pub use save::{
SAVE_FORMATS, SaveJob, SaveMode, SavePlan, StreamWrite, TEMPLATE_FORMATS, export_folder, save_filters, save_format, save_job, save_plan,
save_with, stamp_save_dates, template_filters, templates_dialog_folder, templates_folder,
SAVE_FORMATS, SaveJob, SaveMode, SavePlan, StreamWrite, TEMPLATE_FORMATS, export_folder, remembered_save_options, save_filters, save_format,
save_job, save_plan, save_with, stamp_save_dates, template_filters, templates_dialog_folder, templates_folder,
};
pub use screens::{
PRESET_KEYS as SCREEN_EXPORT_PRESET_KEYS, PRESETS as SCREEN_PRESETS, ScreenExport, ScreenExportPreset, ScreenSize,
+99 -4
View File
@@ -14,7 +14,8 @@ use vectorcraft_doc::Document;
use super::super::*;
use super::{
ArtboardPick, Encoded, Format, Loaded, encode_all, file_stem, format, format_for_name, load, read_file, with_compression_pref, write_encoded,
ARTBOARD_PARAMS, ArtboardPick, Encoded, Format, Loaded, encode_all, file_stem, format, format_for_name, load, read_file, with_compression_pref,
write_encoded,
};
use crate::{DocState, Prefs};
@@ -253,7 +254,7 @@ pub fn save_plan(s: &Session, mode: SaveMode, p: &Value) -> Result<SavePlan> {
}
}
// None given: the ones the document was last saved with in this format.
let options = if given.is_empty() && format.id == st.format { st.save_options.clone() } else { given };
let options = if given.is_empty() && format.id == st.format { remembered_save_options(st) } else { given };
// Save writes the document's own file; the other modes only where they are told to.
let path =
path.or_else(|| (mode == SaveMode::Save && !st.converted && !st.recovered && format.id == st.format).then(|| st.path.clone()).flatten());
@@ -275,6 +276,99 @@ pub fn save_plan(s: &Session, mode: SaveMode, p: &Value) -> Result<SavePlan> {
Ok(SavePlan { mode, path, format, options, name, folder, modified })
}
/// The options `st` was last saved with, their artboards (`artboard`, `artboards` and `range`)
/// following the artboards they named ([`DocState::save_artboards`]): an artboard deleted since is
/// left out, and one that moved is named by its new number. When none of the artboards the save
/// used is left, the pick is left out, and a save without options writes the artboards it writes
/// when none is named. `range` `"all"` stays as it is. Artboards given to a save are still checked.
pub fn remembered_save_options(st: &DocState) -> Map<String, Value> {
let mut options = st.save_options.clone();
let ids: Vec<u32> = st.doc.artboards.iter().map(|a| a.id).collect();
// The encoders refuse a document without artboards themselves.
if ids.is_empty() {
return options;
}
// Artboard `i` of the save, where it is now (by number when the save's artboards are unknown).
let then = &st.save_artboards;
let count = if then.is_empty() { ids.len() } else { then.len() };
let now = |i: usize| match then.get(i) {
Some(id) => ids.iter().position(|x| x == id),
None => (then.is_empty() && i < ids.len()).then_some(i),
};
// Each pick that names artboards: `Some` of where they are now (empty: all gone).
let mut moved: Vec<(&str, Option<Vec<usize>>)> = vec![];
for k in ARTBOARD_PARAMS {
let named = match (k, options.get(k)) {
("range", Some(Value::String(r))) if r.trim().is_empty() || r.trim().eq_ignore_ascii_case("all") => continue,
("range", Some(Value::String(r))) => match vectorcraft_doc::range::parse_range(r, count) {
Ok(v) => v,
Err(_) => {
moved.push((k, Some(vec![])));
continue;
}
},
("artboards", Some(v @ Value::Array(_))) => match serde_json::from_value::<Vec<usize>>(v.clone()) {
Ok(v) => v,
Err(_) => continue,
},
("artboard", Some(v)) => match v.as_u64().and_then(|i| usize::try_from(i).ok()) {
Some(i) => vec![i],
None => continue,
},
_ => continue,
};
let there: Vec<usize> = named.iter().filter_map(|i| now(*i)).collect();
// A range that reads differently against the artboards there are now (`"2-"` once one
// was added) is written out as the artboards it named.
let reads = match (k, options.get(k)) {
("range", Some(Value::String(r))) => vectorcraft_doc::range::parse_range(r, ids.len()).ok(),
_ => Some(named),
};
moved.push((k, (reads.as_ref() != Some(&there)).then_some(there)));
}
// The pick a save uses (as `ArtboardPick::resolve` reads it: `range`, then `artboards`, then
// `artboard`) is gone: so is the rest.
let used = ["range", "artboards", "artboard"].into_iter().find(|k| match options.get(*k) {
Some(Value::String(r)) => !r.trim().is_empty(),
Some(v) => !v.is_null(),
None => false,
});
let gone = |k: &str| moved.iter().any(|(m, there)| *m == k && there.as_ref().is_some_and(Vec::is_empty));
if used.is_some_and(gone) {
for k in ARTBOARD_PARAMS {
options.remove(k);
}
return options;
}
for (k, there) in moved {
let Some(there) = there else { continue };
let value = match (k, there.first()) {
(_, None) => None,
("range", Some(_)) => Some(json!(range_text(&there))),
("artboards", Some(_)) => Some(json!(there)),
(_, Some(i)) => Some(json!(i)),
};
match value {
Some(v) => _ = options.insert(k.into(), v),
None => _ = options.remove(k),
}
}
options
}
/// 0-based artboard indices as a 1-based range in the same order, runs joined (`"1-2, 5"`).
fn range_text(picked: &[usize]) -> String {
let mut runs: Vec<(usize, usize)> = vec![];
for &i in picked {
match runs.last_mut() {
Some((_, end)) if *end + 1 == i => *end = i,
_ => runs.push((i, i)),
}
}
let run = |&(a, b): &(usize, usize)| if a == b { format!("{}", a + 1) } else { format!("{}-{}", a + 1, b + 1) };
runs.iter().map(run).collect::<Vec<_>>().join(", ")
}
/// Does a save in `f` read option `key`? PDF (and a .ai file, a PDF) also reads its General
/// settings (`createLayers`…) and presets.
fn reads_option(f: &Format, key: &str) -> bool {
@@ -664,6 +758,7 @@ impl SaveJob {
st.path = Some(path);
st.format = self.plan.format.id;
st.save_options = self.plan.options;
st.save_artboards = self.snapshot.artboards.iter().map(|a| a.id).collect();
st.converted = false;
st.recovered = false;
if let Some(at) = self.stamped {
@@ -756,14 +851,14 @@ fn format_options(s: &mut Session, p: &Value) -> Result<Value> {
let f = format(id)
.filter(|f| f.write || SAVE_FORMATS.contains(&f.id) || TEMPLATE_FORMATS.contains(&f.id))
.ok_or_else(|| bad(C, format!("`{id}` is no writable format (see document.formats)")))?;
let saved = st.filter(|d| d.format == f.id).map(|d| &d.save_options);
let saved = st.filter(|d| d.format == f.id).map(remembered_save_options);
// A native save compresses as Use Compression says, unless told otherwise.
let prefs = with_compression_pref(&s.prefs, &json!({}));
let mut v = f.to_json();
if let Some(options) = v["options"].as_object_mut() {
for (name, o) in options.iter_mut() {
let default = prefs.get(name).filter(|_| matches!(f.id, "vectorcraft" | "template")).unwrap_or(&o["default"]);
o["value"] = saved.and_then(|m| m.get(name)).unwrap_or(default).clone();
o["value"] = saved.as_ref().and_then(|m| m.get(name)).unwrap_or(default).clone();
}
}
v["saveFormats"] =
+1
View File
@@ -866,6 +866,7 @@ fn artboard_delete(s: &mut Session, p: &Value) -> Result<Value> {
// The last first, so the others keep their places; their guides go with them.
for &i in indices.iter().rev() {
let id = d.artboards.remove(i).id;
d.retire_artboard_id(id);
d.retain_guides(sel, |_, g| g.artboard != Some(id));
}
Ok(())
+3 -1
View File
@@ -969,10 +969,12 @@ fn convert_to_artboards(s: &mut Session, _: &Value) -> Result<Value> {
if b.width() <= 0.0 || b.height() <= 0.0 {
continue;
}
// Named one past the highest artboard id the document has, whatever id it is given.
let number = d.artboards.iter().map(|a| a.id).max().map_or(1, |m| m.saturating_add(1));
let aid = d.next_artboard_id();
d.artboards.push(vectorcraft_doc::Artboard {
id: aid,
name: format!("Artboard {aid}"),
name: format!("Artboard {number}"),
rect: b,
locked: false,
show_center_mark: false,
+1
View File
@@ -208,6 +208,7 @@ pub(crate) fn artboard_copy(s: &mut Session, p: &Value, cut: bool) -> Result<Val
// The last first, so the others keep their places; their guides go with them.
for &i in indices.iter().rev() {
let gone = d.artboards.remove(i).id;
d.retire_artboard_id(gone);
d.retain_guides(sel, |_, g| g.artboard != Some(gone));
}
d.remove_many(&art)?;
+16
View File
@@ -378,6 +378,10 @@ pub struct DocState {
/// That format's options as last saved (SVG options for SVG, the Save PDF settings for PDF;
/// empty for native files): Save reuses them and `file.formatOptions` reads them back.
pub save_options: serde_json::Map<String, Value>,
/// The ids of the document's artboards, in order, when `save_options` were stored: the
/// artboards their `artboard`, `artboards` and `range` named by number then
/// ([`cmd::fileio::remembered_save_options`]).
pub save_artboards: Vec<u32>,
/// Opened from an older native file (former name or format version): the title says
/// "[Converted]" and Save asks for a new name instead of overwriting it.
pub converted: bool,
@@ -569,6 +573,7 @@ impl DocState {
transparency_grid,
format: "vectorcraft",
save_options: Default::default(),
save_artboards: vec![],
converted: false,
view,
recovered: false,
@@ -1682,6 +1687,17 @@ impl Session {
st.revision = old.revision + 1;
st.format = old.format;
st.save_options = old.save_options.clone();
// The remembered artboard pick follows the artboards it named when the file read back has
// the artboards the document had at the save, in the same order and with the same ids (a
// native file keeps them; a PDF's pages get new ids in page order); otherwise it is left
// out.
if old.save_artboards.is_empty() || st.doc.artboards.iter().map(|a| a.id).eq(old.save_artboards.iter().copied()) {
st.save_artboards = old.save_artboards.clone();
} else {
for k in cmd::fileio::ARTBOARD_PARAMS {
st.save_options.remove(k);
}
}
st.converted = old.converted;
st.imported_from = old.imported_from.clone();
st.import_losses = old.import_losses.clone();
+18
View File
@@ -977,6 +977,24 @@ fn convert_to_artboards_and_rearrange() {
assert_eq!(bounds(&s, b), Rect::new(50.0, 675.0, 60.0, 685.0));
}
/// Convert to Artboards names an artboard one past the highest artboard id the document has, also
/// after a delete of the last artboard or an undo of a new one.
#[test]
fn convert_to_artboards_names_by_the_artboards_the_document_has() {
let name_after = |change: &dyn Fn(&mut Session)| {
let mut s = session();
s.execute("artboard.new", &json!({})).unwrap();
s.execute("artboard.new", &json!({})).unwrap();
change(&mut s);
let a = rect(&mut s, 3000.0, 0.0, 200.0, 100.0);
sel(&mut s, &[a]);
s.execute("artboard.convertToArtboards", &json!({})).unwrap();
s.doc().unwrap().doc.artboards.last().map(|b| b.name.clone()).unwrap()
};
assert_eq!(name_after(&|s| _ = s.execute("artboard.delete", &json!({"index": 2})).unwrap()), "Artboard 3");
assert_eq!(name_after(&|s| _ = s.execute("edit.undo", &json!({})).unwrap()), "Artboard 3");
}
/// Rearrange All Artboards refuses `columns` that are not a whole number (a fraction, a negative number or a string),
/// and the artboards stay where they were; 1.0, as the Rearrange All Artboards dialog sends a count, is one column.
#[test]
+124
View File
@@ -188,6 +188,130 @@ fn ai_without_pdf_content_and_compression() {
assert!(loose.len() > packed.len(), "{} vs {}", loose.len(), packed.len());
}
#[test]
fn a_remembered_artboard_the_document_no_longer_has_is_dropped() {
let dir = Folder::new("remembered-artboards");
for (name, p) in [
("legacy.ai", json!({"aiVersion": "7", "range": "3"})),
("separate.ai", json!({"separateArtboards": true, "range": "3"})),
("separate.vectorcraft", json!({"separateArtboards": true, "range": "1, 3"})),
("doc.pdf", json!({"range": "3"})),
("doc.svg", json!({"artboard": 2})),
] {
let mut s = three_boards();
let mut first = p.clone();
first["path"] = json!(dir.file(name));
s.execute("file.saveAs", &first).unwrap_or_else(|e| panic!("{name}: {e}"));
s.execute("artboard.delete", &json!({"index": 2})).unwrap();
let v = s.execute("file.formatOptions", &json!({})).unwrap();
for k in ["artboard", "range"].iter().filter(|k| p.get(**k).is_some()) {
assert_ne!(v["options"][*k]["value"], p[*k], "{name}: Save no longer uses {k}");
}
s.execute("document.save", &json!({})).unwrap_or_else(|e| panic!("{name}: {e}"));
// Given to the save, it is still refused.
let e = s.execute("document.save", &p).unwrap_err().to_string();
assert!(e.contains("from 1 to 2") || e.contains("no artboard 2"), "{name}: {e}");
}
// A remembered artboard that is still there is kept.
let mut s = three_boards();
s.execute("file.saveAs", &json!({"path": dir.file("kept.ai"), "aiVersion": "7", "range": "2"})).unwrap();
s.execute("artboard.delete", &json!({"index": 2})).unwrap();
let v = s.execute("file.formatOptions", &json!({})).unwrap();
assert_eq!(v["options"]["range"]["value"], json!("2"));
let r = s.execute("document.save", &json!({})).unwrap();
assert!(r["path"].as_str().is_some_and(|p| p.ends_with("kept.ai")), "{r}");
}
#[test]
fn a_remembered_artboard_pick_follows_the_artboards_it_named() {
let dir = Folder::new("remembered-pick");
// Save each artboard of A, B and C to a file with `range`, change the artboards, then save
// without options: the range Save now uses and the artboards it writes, by file name.
let save = |range: &str, change: &[(&str, Value)]| -> (Value, Vec<String>) {
let mut s = three_boards();
let path = dir.file("x.ai");
s.execute("file.saveAs", &json!({"path": path, "separateArtboards": true, "range": range})).unwrap();
for (cmd, p) in change {
s.execute(cmd, p).unwrap();
}
let range = s.execute("file.formatOptions", &json!({})).unwrap()["options"]["range"]["value"].clone();
let r = s.execute("document.save", &json!({})).unwrap();
let files = r["files"].as_array().unwrap().iter().skip(1).map(|f| f.as_str().unwrap().rsplit(['/', '\\']).next().unwrap().to_string());
(range, files.collect())
};
let delete = |i: usize| ("artboard.delete", json!({"index": i}));
assert_eq!(save("1, 3", &[delete(2)]), (json!("1"), vec!["x-A.ai".to_string()]));
assert_eq!(save("1, 3", &[delete(0)]), (json!("2"), vec!["x-C.ai".to_string()]));
assert_eq!(save("1-3", &[delete(1)]), (json!("1-2"), vec!["x-A.ai".to_string(), "x-C.ai".to_string()]));
let (range, mut files) = save("1, 3", &[("artboard.reorder", json!({"index": 2, "to": 0}))]);
files.sort();
assert_eq!((range, files), (json!("2, 1"), vec!["x-A.ai".to_string(), "x-C.ai".to_string()]));
// None of them left: every artboard, as when none is named.
assert_eq!(save("1, 3", &[delete(2), delete(0)]), (Value::Null, vec!["x-B.ai".to_string()]));
assert_eq!(save("all", &[delete(0)]), (json!("all"), vec!["x-B.ai".to_string(), "x-C.ai".to_string()]));
// An artboard made after one was deleted is not taken for it.
let new = ("artboard.new", json!({"x": 600, "y": 0, "width": 100, "height": 100, "name": "D"}));
let all = ["x-A.ai", "x-B.ai", "x-D.ai"].map(String::from).to_vec();
assert_eq!(save("3", &[delete(2), new.clone()]), (Value::Null, all));
assert_eq!(save("1, 3", &[delete(2), new.clone()]), (json!("1"), vec!["x-A.ai".to_string()]));
// A range open at its end keeps to the artboards it named.
assert_eq!(save("2-", std::slice::from_ref(&new)), (json!("2-3"), vec!["x-B.ai".to_string(), "x-C.ai".to_string()]));
// Nor is one made after undoing the artboard (and the three rectangles made after it).
let undo = ("edit.undo", json!({}));
let undone = [undo.clone(), undo.clone(), undo.clone(), undo, new];
assert_eq!(save("1, 3", &undone), (json!("1"), vec!["x-A.ai".to_string()]));
// The other saves that remember a pick: a PDF's range, an SVG's artboard, a legacy .ai's
// range and a native file's separate files.
for (name, p, gone, after) in [
("doc.pdf", json!({"range": "1, 3"}), 0, json!({"range": "2"})),
// `range` wins over `artboard`, which a save then does not use.
("unused.pdf", json!({"artboard": 0, "range": "3"}), 0, json!({"range": "2"})),
("doc.svg", json!({"artboard": 2}), 0, json!({"artboard": 1})),
("legacy.ai", json!({"aiVersion": "7", "range": "1, 3"}), 2, json!({"range": "1"})),
("doc.vectorcraft", json!({"separateArtboards": true, "range": "1, 3"}), 0, json!({"range": "2"})),
] {
let mut s = three_boards();
let mut first = p.clone();
first["path"] = json!(dir.file(name));
s.execute("file.saveAs", &first).unwrap();
s.execute("artboard.delete", &json!({"index": gone})).unwrap();
let v = s.execute("file.formatOptions", &json!({})).unwrap();
for (k, value) in after.as_object().unwrap() {
assert_eq!(&v["options"][k]["value"], value, "{name}: {k}");
}
s.execute("document.save", &json!({})).unwrap_or_else(|e| panic!("{name}: {e}"));
if name.ends_with(".pdf") {
let bytes = std::fs::read(dir.file(name)).unwrap();
let info = s.execute("document.pdfInfo", &json!({"dataBase64": vectorcraft_format::base64_encode(&bytes)})).unwrap();
assert_eq!(info["pages"], 1, "only the artboard that was 3");
}
}
// File > Revert: a PDF holds only the pages it was saved with, so its pick is left out; a
// native file keeps every artboard, and the pick stays.
let mut s = three_boards();
s.execute("file.saveAs", &json!({"path": dir.file("revert.pdf"), "range": "1, 3"})).unwrap();
s.execute("shape.rectangle", &json!({"x": 10, "y": 50, "width": 30, "height": 30})).unwrap();
s.execute("file.revert", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc.artboards.len(), 2);
assert_eq!(s.execute("file.formatOptions", &json!({})).unwrap()["options"]["range"]["value"], Value::Null);
// A PDF without the editable document reads back with new ids in page order: after a reorder,
// the pick is left out, not taken for another reorder.
let mut s = three_boards();
s.execute("artboard.reorder", &json!({"index": 2, "to": 0})).unwrap();
s.execute("file.saveAs", &json!({"path": dir.file("reordered.pdf"), "range": "1-3", "preserveEditing": false})).unwrap();
s.execute("shape.rectangle", &json!({"x": 10, "y": 50, "width": 30, "height": 30})).unwrap();
s.execute("file.revert", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc.artboards.iter().map(|a| a.id).collect::<Vec<_>>(), [1, 2, 3]);
assert_eq!(s.execute("file.formatOptions", &json!({})).unwrap()["options"]["range"]["value"], Value::Null);
let mut s = three_boards();
s.execute("file.saveAs", &json!({"path": dir.file("revert.vectorcraft"), "separateArtboards": true, "range": "1, 3"})).unwrap();
s.execute("artboard.delete", &json!({"index": 0})).unwrap();
s.execute("file.revert", &json!({})).unwrap();
assert_eq!(s.execute("file.formatOptions", &json!({})).unwrap()["options"]["range"]["value"], json!("1, 3"));
}
#[test]
fn format_options_list_the_save_options() {
let mut s = three_boards();
+13 -1
View File
@@ -92,7 +92,7 @@ pub fn open(app: &mut VectorcraftApp, mode: Mode, path: Option<&str>) {
.session
.active()
.filter(|st| mode != Mode::Export && matches!(st.format, "svg" | "svgz"))
.map(|st| Value::Object(st.save_options.clone()));
.map(|st| Value::Object(vectorcraft_engine::cmd::fileio::remembered_save_options(st)));
for last in [&app.ui.svg_options].into_iter().chain(saved.as_ref()) {
if let Some(o) = last.as_object() {
fields.extend(o.iter().map(|(k, v)| (k.clone(), v.clone())));
@@ -416,6 +416,18 @@ mod tests {
assert!(vectorcraft_format::sniff(&written.borrow()[3].1));
}
#[test]
fn save_as_svg_leaves_out_a_remembered_artboard_the_document_no_longer_has() {
let (mut app, written) = app("/tmp/doc.svg");
app.run("file.saveAs", json!({"path": "/tmp/doc.svg", "svg": {"artboard": 1}})).unwrap();
assert_eq!(app.session.active().unwrap().save_options["artboard"], 1);
app.run("artboard.delete", json!({"index": 1})).unwrap();
app.run("file.saveAs", Value::Null).unwrap();
assert!(app.ui.dialog.as_ref().unwrap().fields.get("artboard").is_none());
super::super::confirm(&mut app).unwrap();
assert_eq!((written.borrow().len(), written.borrow()[1].0.as_str()), (2, "/tmp/doc.svg"));
}
#[test]
fn profile_encoding_and_embedded_fonts_reach_the_file() {
let (mut app, written) = app("/tmp/tiny.svg");