mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 09:59:47 -06:00
The Package status and question, the missing-links note, the Missing Fonts
and Find Linked Files search counts, the modified-links questions, Apply to
All, and the CSS Properties panel's notes no longer use a fixed "(s)" or
plural template. They use the plural rows of each catalog, so "1 linked file
not found" and Czech "Nenalezeny 2 propojené soubory" read correctly.
The searched line is built from two plural phrases, "{n} folder" and
"{n} file", in a "{folders} and {files} searched" frame. Package rows that
joined the note to the folder without a space now keep the space.
149 lines
6.4 KiB
Rust
149 lines
6.4 KiB
Rust
//! The CSS Properties panel drawn headless: the selection's CSS in the code view, Generate CSS
|
||
//! for the whole document, the export options in the panel menu, and Copy Selected Style and
|
||
//! Export through their UI commands.
|
||
|
||
use std::cell::RefCell;
|
||
use std::rc::Rc;
|
||
|
||
use serde_json::json;
|
||
use vectorcraft_engine::Session;
|
||
use vectorcraft_svg::{CssOptions, CssUnits};
|
||
|
||
use super::css_properties::{ALL, ID, OPTIONS, menu, show};
|
||
use super::set_pstate;
|
||
use crate::tests_labels::painted_text;
|
||
use crate::{Services, VectorcraftApp};
|
||
|
||
type Log = Rc<RefCell<Vec<(String, Vec<u8>)>>>;
|
||
|
||
/// An app with a selected red 30 × 20 rectangle; files written go to the log.
|
||
fn app() -> (VectorcraftApp, Log) {
|
||
let log = Log::default();
|
||
let l = log.clone();
|
||
let services = Services {
|
||
write: Some(Box::new(move |path: &str, bytes: &[u8]| {
|
||
l.borrow_mut().push((path.to_string(), bytes.to_vec()));
|
||
Ok(())
|
||
})),
|
||
..Services::default()
|
||
};
|
||
let mut app = VectorcraftApp::new(Session::new(), services);
|
||
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap();
|
||
app.run("paint.setFill", json!({"color": "#ff0000"})).unwrap();
|
||
(app, log)
|
||
}
|
||
|
||
#[test]
|
||
fn the_code_view_shows_the_selections_css_with_the_options() {
|
||
let (mut app, _) = app();
|
||
assert!(crate::state::ICON_PANELS.iter().any(|(id, ..)| *id == ID));
|
||
let text = painted_text(&mut app, show);
|
||
assert!(text.contains(".rectangle {") && text.contains("background-color: #ff0000;") && text.contains("width: 30px;"), "{text}");
|
||
let text = painted_text(&mut app, |app, ui| {
|
||
set_pstate(ui.ctx(), OPTIONS, CssOptions { units: CssUnits::Pt, position: true, ..CssOptions::default() });
|
||
show(app, ui);
|
||
});
|
||
assert!(text.contains("width: 30pt;") && text.contains("left: 10pt;"), "{text}");
|
||
}
|
||
|
||
#[test]
|
||
fn generate_css_shows_the_document_while_nothing_is_selected() {
|
||
let (mut app, _) = app();
|
||
app.run("shape.ellipse", json!({"x": 100, "y": 10, "width": 20, "height": 20})).unwrap();
|
||
app.run("select.none", json!({})).unwrap();
|
||
let text = painted_text(&mut app, show);
|
||
assert!(text.contains("No selection") && !text.contains(".rectangle"), "{text}");
|
||
let text = painted_text(&mut app, |app, ui| {
|
||
set_pstate(ui.ctx(), ALL, true);
|
||
show(app, ui);
|
||
});
|
||
assert!(text.contains(".rectangle {") && text.contains(".ellipse {") && text.contains("border-radius: 50%;"), "{text}");
|
||
}
|
||
|
||
/// The note on unnamed objects left out uses the form the language's plural rule picks for the
|
||
/// count.
|
||
#[test]
|
||
fn the_unnamed_objects_note_uses_each_count_form() {
|
||
let note = |code: &str, n: u64| {
|
||
let (mut app, _) = app();
|
||
for i in 1..n {
|
||
app.run("shape.rectangle", json!({"x": 10 + 40 * i, "y": 50, "width": 30, "height": 20})).unwrap();
|
||
}
|
||
app.run("select.all", json!({})).unwrap();
|
||
crate::i18n::set_current(crate::i18n::Lang::from_code(code).unwrap());
|
||
let text = painted_text(&mut app, |app, ui| {
|
||
set_pstate(ui.ctx(), OPTIONS, CssOptions { unnamed: false, ..CssOptions::default() });
|
||
show(app, ui);
|
||
});
|
||
crate::i18n::set_current(crate::i18n::Lang::EN);
|
||
text
|
||
};
|
||
let text = note("en", 1);
|
||
assert!(text.contains("1 unnamed object left out: name it,"), "{text}");
|
||
for (n, want) in [(1, "Vynechán 1 nepojmenovaný objekt."), (2, "Vynechány 2 nepojmenované objekty."), (5, "Vynecháno 5 nepojmenovaných objektů.")]
|
||
{
|
||
let text = note("cs", n);
|
||
assert!(text.contains(want), "{n}: {text}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_panel_menu_lists_the_actions_and_options() {
|
||
let (mut app, _) = app();
|
||
let text = painted_text(&mut app, menu);
|
||
for item in [
|
||
"Copy Selected Style",
|
||
"Export Selected CSS…",
|
||
"Export All…",
|
||
"Generate CSS",
|
||
"Units: px",
|
||
"Absolute Position",
|
||
"Width and Height",
|
||
"Unnamed Objects",
|
||
"Rasterize Unsupported Art",
|
||
] {
|
||
assert!(text.contains(item), "{item} in {text}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn copy_and_export_go_through_the_ui_commands() {
|
||
let (mut app, log) = app();
|
||
let r = app.run("css.copy", json!({})).unwrap();
|
||
assert_eq!(r["rules"], json!(1));
|
||
assert!(app.clipboard_out.as_deref().is_some_and(|c| c.starts_with(".rectangle {") && c.contains("#ff0000")));
|
||
app.run("select.none", json!({})).unwrap();
|
||
assert!(app.run("css.copy", json!({})).is_err(), "nothing selected: nothing to copy");
|
||
// Export All with a star rasterized: the sheet and the star's picture beside it.
|
||
app.run("shape.star", json!({"cx": 100, "cy": 50, "radius1": 30, "radius2": 15})).unwrap();
|
||
let r = app.run("css.exportFile", json!({"path": "/tmp/site/site.css", "scope": "all", "rasterize": true})).unwrap();
|
||
assert_eq!(r["rules"], json!(2));
|
||
let png = std::path::Path::new("/tmp/site").join("path.png").to_string_lossy().into_owned();
|
||
assert_eq!(r["images"], json!([png]));
|
||
let log = log.borrow();
|
||
let names: Vec<&str> = log.iter().map(|(p, _)| p.as_str()).collect();
|
||
assert_eq!(names, ["/tmp/site/site.css", png.as_str()]);
|
||
let css = String::from_utf8(log[0].1.clone()).unwrap();
|
||
assert!(css.contains(".rectangle {") && css.contains("background-image: url(path.png);"), "{css}");
|
||
assert!(log[1].1.starts_with(b"\x89PNG"));
|
||
}
|
||
|
||
#[test]
|
||
fn export_says_in_the_status_bar_when_an_image_could_not_be_decoded() {
|
||
const WARNING: &str = "an image couldn't be decoded and is missing from the picture";
|
||
let (mut app, log) = app();
|
||
app.run("css.exportFile", json!({"path": "/tmp/site/site.css", "scope": "all", "rasterize": true})).unwrap();
|
||
assert_eq!(app.ui.status, "Exported /tmp/site/site.css");
|
||
// A PNG whose header reads but whose pixel data is cut short.
|
||
let mut png = Vec::new();
|
||
image::RgbaImage::from_pixel(64, 32, image::Rgba([200, 30, 30, 255]))
|
||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||
.unwrap();
|
||
vectorcraft_testkit::fixtures::place_cut_png(&mut app.session, &png, json!({}));
|
||
let r = app.run("css.exportFile", json!({"path": "/tmp/site/site.css", "scope": "all", "rasterize": true})).unwrap();
|
||
assert_eq!(r["warnings"], json!([WARNING]));
|
||
assert_eq!(app.ui.status, format!("Exported /tmp/site/site.css with 1 note(s): {WARNING}"));
|
||
assert_eq!(log.borrow().len(), 3, "the sheet and the image's picture are still written");
|
||
}
|