summaryrefslogtreecommitdiff
path: root/rust/crates/graphics/tests
diff options
context:
space:
mode:
Diffstat (limited to 'rust/crates/graphics/tests')
-rw-r--r--rust/crates/graphics/tests/gradient_parser.rs375
-rw-r--r--rust/crates/graphics/tests/gradient_stops.rs176
-rw-r--r--rust/crates/graphics/tests/graphics_render.rs261
3 files changed, 812 insertions, 0 deletions
diff --git a/rust/crates/graphics/tests/gradient_parser.rs b/rust/crates/graphics/tests/gradient_parser.rs
new file mode 100644
index 00000000..4e905ee9
--- /dev/null
+++ b/rust/crates/graphics/tests/gradient_parser.rs
@@ -0,0 +1,375 @@
+use graphics::gradient::ast::*;
+use graphics::gradient::parser::parse_gradient;
+
+fn single(code: &str) -> GradientAst {
+ let ast = parse_gradient(code).unwrap_or_else(|err| panic!("failed to parse {code:?}: {err}"));
+ assert_eq!(ast.len(), 1, "expected one gradient in {code:?}");
+ ast.into_iter().next().unwrap()
+}
+
+#[test]
+fn linear_minimal() {
+ let gradient = single("linear-gradient(red, blue)");
+ assert_eq!(gradient.kind, GradientKind::Linear);
+ assert_eq!(gradient.orientation, None);
+ assert_eq!(
+ gradient.color_stops,
+ vec![
+ ColorStop { color: Color::Literal("red".into()), length: None },
+ ColorStop { color: Color::Literal("blue".into()), length: None },
+ ]
+ );
+}
+
+#[test]
+fn linear_directional() {
+ let gradient = single("linear-gradient(to right, #fff, #000)");
+ assert_eq!(
+ gradient.orientation,
+ Some(GradientOrientation::Linear(LinearOrientation::Directional(
+ "right".into()
+ )))
+ );
+ assert_eq!(gradient.color_stops[0].color, Color::Hex("fff".into()));
+}
+
+#[test]
+fn linear_corner() {
+ let gradient = single("linear-gradient(to left top, red, blue)");
+ assert_eq!(
+ gradient.orientation,
+ Some(GradientOrientation::Linear(LinearOrientation::Directional(
+ "left top".into()
+ )))
+ );
+ let gradient = single("linear-gradient(to bottom right, red, blue)");
+ assert_eq!(
+ gradient.orientation,
+ Some(GradientOrientation::Linear(LinearOrientation::Directional(
+ "bottom right".into()
+ )))
+ );
+}
+
+#[test]
+fn linear_angles() {
+ let gradient = single("linear-gradient(45deg, red, blue)");
+ assert_eq!(
+ gradient.orientation,
+ Some(GradientOrientation::Linear(LinearOrientation::Angular(
+ "45".into()
+ )))
+ );
+ let gradient = single("linear-gradient(-90deg, red, blue)");
+ assert_eq!(
+ gradient.orientation,
+ Some(GradientOrientation::Linear(LinearOrientation::Angular(
+ "-90".into()
+ )))
+ );
+ let gradient = single("linear-gradient(0.5rad, red, blue)");
+ assert_eq!(
+ gradient.orientation,
+ Some(GradientOrientation::Linear(LinearOrientation::Angular(
+ "0.5".into()
+ )))
+ );
+ let gradient = single("linear-gradient(.25deg, red, blue)");
+ assert_eq!(
+ gradient.orientation,
+ Some(GradientOrientation::Linear(LinearOrientation::Angular(
+ ".25".into()
+ )))
+ );
+}
+
+#[test]
+fn linear_legacy_direction() {
+ let gradient = single("linear-gradient(left, red, blue)");
+ assert_eq!(
+ gradient.orientation,
+ Some(GradientOrientation::Linear(LinearOrientation::Directional(
+ "left".into()
+ )))
+ );
+}
+
+#[test]
+fn repeating_and_vendor_prefix() {
+ assert_eq!(
+ single("repeating-linear-gradient(red, blue)").kind,
+ GradientKind::RepeatingLinear
+ );
+ assert_eq!(
+ single("repeating-radial-gradient(red, blue)").kind,
+ GradientKind::RepeatingRadial
+ );
+ assert_eq!(
+ single("-webkit-linear-gradient(red, blue)").kind,
+ GradientKind::Linear
+ );
+ assert_eq!(
+ single("-moz-radial-gradient(red, blue)").kind,
+ GradientKind::Radial
+ );
+ assert_eq!(
+ single("LINEAR-GRADIENT(red, blue)").kind,
+ GradientKind::Linear
+ );
+}
+
+#[test]
+fn radial_shapes() {
+ let gradient = single("radial-gradient(circle, red, blue)");
+ assert_eq!(
+ gradient.orientation,
+ Some(GradientOrientation::Radial(vec![RadialOrientation::Shape(
+ Shape { value: ShapeKind::Circle, style: None, at: None }
+ )]))
+ );
+
+ let gradient = single("radial-gradient(circle 50px, red, blue)");
+ assert_eq!(
+ gradient.orientation,
+ Some(GradientOrientation::Radial(vec![RadialOrientation::Shape(
+ Shape {
+ value: ShapeKind::Circle,
+ style: Some(ShapeStyle::Distance(Distance::Px("50".into()))),
+ at: None,
+ }
+ )]))
+ );
+
+ let gradient = single("radial-gradient(ellipse closest-side, red, blue)");
+ assert_eq!(
+ gradient.orientation,
+ Some(GradientOrientation::Radial(vec![RadialOrientation::Shape(
+ Shape {
+ value: ShapeKind::Ellipse,
+ style: Some(ShapeStyle::ExtentKeyword("closest-side".into())),
+ at: None,
+ }
+ )]))
+ );
+}
+
+#[test]
+fn radial_with_position() {
+ let gradient = single("radial-gradient(circle at 25% 25%, red, blue)");
+ assert_eq!(
+ gradient.orientation,
+ Some(GradientOrientation::Radial(vec![RadialOrientation::Shape(
+ Shape {
+ value: ShapeKind::Circle,
+ style: None,
+ at: Some(Position {
+ x: Some(Distance::Percent("25".into())),
+ y: Some(Distance::Percent("25".into())),
+ }),
+ }
+ )]))
+ );
+
+ let gradient = single("radial-gradient(farthest-corner at 10px 2em, red, blue)");
+ assert_eq!(
+ gradient.orientation,
+ Some(GradientOrientation::Radial(vec![
+ RadialOrientation::ExtentKeyword {
+ value: "farthest-corner".into(),
+ at: Some(Position {
+ x: Some(Distance::Px("10".into())),
+ y: Some(Distance::Em("2".into())),
+ }),
+ }
+ ]))
+ );
+
+ let gradient = single("radial-gradient(at center, red, blue)");
+ assert_eq!(
+ gradient.orientation,
+ Some(GradientOrientation::Radial(vec![
+ RadialOrientation::DefaultRadial {
+ at: Position {
+ x: Some(Distance::PositionKeyword("center".into())),
+ y: None,
+ },
+ }
+ ]))
+ );
+
+ let gradient = single("radial-gradient(100px 50px at 10% 20%, red, blue)");
+ assert_eq!(
+ gradient.orientation,
+ Some(GradientOrientation::Radial(vec![RadialOrientation::Shape(
+ Shape {
+ value: ShapeKind::Ellipse,
+ style: Some(ShapeStyle::Position(Position {
+ x: Some(Distance::Px("100".into())),
+ y: Some(Distance::Px("50".into())),
+ })),
+ at: Some(Position {
+ x: Some(Distance::Percent("10".into())),
+ y: Some(Distance::Percent("20".into())),
+ }),
+ }
+ )]))
+ );
+}
+
+#[test]
+fn radial_multiple_orientations() {
+ let gradient = single("radial-gradient(circle, ellipse, red, blue)");
+ match gradient.orientation {
+ Some(GradientOrientation::Radial(list)) => assert_eq!(list.len(), 2),
+ other => panic!("unexpected orientation: {other:?}"),
+ }
+}
+
+#[test]
+fn color_formats() {
+ let gradient = single("linear-gradient(#abcdef, rgb(1,2,3), rgba(1,2,3,0.5), hsl(120,50%,50%), hsla(120,50%,50%,0.5), var(--brand), rebeccapurple)");
+ assert_eq!(gradient.color_stops[0].color, Color::Hex("abcdef".into()));
+ assert_eq!(
+ gradient.color_stops[1].color,
+ Color::Rgb(vec!["1".into(), "2".into(), "3".into()])
+ );
+ assert_eq!(
+ gradient.color_stops[2].color,
+ Color::Rgba(vec!["1".into(), "2".into(), "3".into(), "0.5".into()])
+ );
+ assert_eq!(
+ gradient.color_stops[3].color,
+ Color::Hsl(["120".into(), "50".into(), "50".into()])
+ );
+ assert_eq!(
+ gradient.color_stops[4].color,
+ Color::Hsla(["120".into(), "50".into(), "50".into(), "0.5".into()])
+ );
+ assert_eq!(gradient.color_stops[5].color, Color::Var("--brand".into()));
+ assert_eq!(
+ gradient.color_stops[6].color,
+ Color::Literal("rebeccapurple".into())
+ );
+}
+
+#[test]
+fn stop_lengths() {
+ let gradient = single("linear-gradient(red 10%, blue 20px, green 3em, white calc(10% + 5px))");
+ assert_eq!(
+ gradient.color_stops[0].length,
+ Some(Distance::Percent("10".into()))
+ );
+ assert_eq!(
+ gradient.color_stops[1].length,
+ Some(Distance::Px("20".into()))
+ );
+ assert_eq!(
+ gradient.color_stops[2].length,
+ Some(Distance::Em("3".into()))
+ );
+ assert_eq!(
+ gradient.color_stops[3].length,
+ Some(Distance::Calc("10% + 5px".into()))
+ );
+}
+
+#[test]
+fn multiple_gradients() {
+ let ast = parse_gradient("linear-gradient(red, blue), radial-gradient(circle, white, black)")
+ .unwrap();
+ assert_eq!(ast.len(), 2);
+ assert_eq!(ast[0].kind, GradientKind::Linear);
+ assert_eq!(ast[1].kind, GradientKind::Radial);
+}
+
+#[test]
+fn trailing_semicolon_and_whitespace() {
+ let ast = parse_gradient(" linear-gradient(red, blue); ").unwrap();
+ assert_eq!(ast.len(), 1);
+}
+
+#[test]
+fn empty_input_is_empty_list() {
+ assert_eq!(parse_gradient("").unwrap(), vec![]);
+}
+
+#[test]
+fn double_space_directional_quirk() {
+ let gradient = single("linear-gradient(to right, red, blue)");
+ assert_eq!(gradient.orientation, None);
+ assert_eq!(gradient.color_stops[0].color, Color::Literal("to".into()));
+ assert_eq!(
+ gradient.color_stops[0].length,
+ Some(Distance::PositionKeyword("right".into()))
+ );
+}
+
+#[test]
+fn error_missing_close_paren() {
+ let err = parse_gradient("linear-gradient(red, blue").unwrap_err();
+ assert_eq!(err.message, "Missing )");
+}
+
+#[test]
+fn error_extra_comma() {
+ let err = parse_gradient("linear-gradient(red,,blue)").unwrap_err();
+ assert_eq!(err.message, "Expected color definition");
+}
+
+#[test]
+fn error_missing_comma_before_color_stops() {
+ let err = parse_gradient("linear-gradient(to left red, blue)").unwrap_err();
+ assert_eq!(err.message, "Missing comma before color stops");
+}
+
+#[test]
+fn error_invalid_input_not_eof() {
+ let err = parse_gradient("foo(red)").unwrap_err();
+ assert_eq!(err.message, "Invalid input not EOF");
+ assert_eq!(err.input, "foo(red)");
+}
+
+#[test]
+fn error_hsl_hue_percentage() {
+ let err = parse_gradient("linear-gradient(hsl(50%, 50%, 50%), red)").unwrap_err();
+ assert!(err.message.starts_with("HSL hue value must be a number"));
+}
+
+#[test]
+fn error_hsl_missing_percentages() {
+ let err = parse_gradient("linear-gradient(hsl(120, 50, 50), red)").unwrap_err();
+ assert_eq!(
+ err.message,
+ "Expected percentage value for saturation and lightness in HSL"
+ );
+}
+
+#[test]
+fn error_rgb_empty() {
+ let err = parse_gradient("linear-gradient(rgb(), red)").unwrap_err();
+ assert_eq!(err.message, "Expected number");
+}
+
+#[test]
+fn error_calc_unbalanced() {
+ let err = parse_gradient("linear-gradient(red calc(10%").unwrap_err();
+ assert_eq!(err.message, "Missing closing parenthesis in calc() expression");
+}
+
+#[test]
+fn error_var_missing_name() {
+ let err = parse_gradient("linear-gradient(var(), red)").unwrap_err();
+ assert_eq!(err.message, "Expected CSS variable name");
+}
+
+#[test]
+fn error_at_missing_position() {
+ let err = parse_gradient("radial-gradient(circle at, red, blue)").unwrap_err();
+ assert_eq!(err.message, "Missing positioning value");
+}
+
+#[test]
+fn error_display_format() {
+ let err = parse_gradient("foo()").unwrap_err();
+ assert_eq!(err.to_string(), "foo(): Invalid input not EOF");
+}
diff --git a/rust/crates/graphics/tests/gradient_stops.rs b/rust/crates/graphics/tests/gradient_stops.rs
new file mode 100644
index 00000000..04adc893
--- /dev/null
+++ b/rust/crates/graphics/tests/gradient_stops.rs
@@ -0,0 +1,176 @@
+use graphics::gradient::ast::{Color, ColorStop, Distance};
+use graphics::gradient::stops::{color_to_string, normalize_color_stops};
+
+fn stop(color: Color, length: Option<Distance>) -> ColorStop {
+ ColorStop { color, length }
+}
+
+fn literal(name: &str) -> Color {
+ Color::Literal(name.to_string())
+}
+
+#[test]
+fn color_string_round_trip() {
+ assert_eq!(color_to_string(&Color::Hex("abc".into())), "#abc");
+ assert_eq!(color_to_string(&literal("red")), "red");
+ assert_eq!(
+ color_to_string(&Color::Rgb(vec!["1".into(), "2".into(), "3".into()])),
+ "rgb(1,2,3)"
+ );
+ assert_eq!(
+ color_to_string(&Color::Rgba(vec!["1".into(), "2".into(), "3".into(), "0.5".into()])),
+ "rgba(1,2,3,0.5)"
+ );
+ assert_eq!(
+ color_to_string(&Color::Hsl(["120".into(), "50".into(), "50".into()])),
+ "hsl(120,50,50)"
+ );
+ assert_eq!(
+ color_to_string(&Color::Hsla(["120".into(), "50".into(), "50".into(), "0.5".into()])),
+ "hsla(120,50,50,0.5)"
+ );
+ assert_eq!(color_to_string(&Color::Var("--x".into())), "var(--x)");
+}
+
+#[test]
+fn evenly_distributed_stops() {
+ let stops = normalize_color_stops(
+ &[stop(literal("red"), None), stop(literal("blue"), None)],
+ 100.0,
+ );
+ assert_eq!(stops[0].offset, 0.0);
+ assert_eq!(stops[1].offset, 1.0);
+
+ let stops = normalize_color_stops(
+ &[
+ stop(literal("a"), None),
+ stop(literal("b"), None),
+ stop(literal("c"), None),
+ ],
+ 100.0,
+ );
+ assert_eq!(stops[1].offset, 0.5);
+}
+
+#[test]
+fn explicit_and_mixed_offsets() {
+ let stops = normalize_color_stops(
+ &[
+ stop(literal("red"), Some(Distance::Percent("20".into()))),
+ stop(literal("green"), None),
+ stop(literal("blue"), Some(Distance::Percent("80".into()))),
+ ],
+ 100.0,
+ );
+ assert_eq!(stops[0].offset, 0.2);
+ assert_eq!(stops[1].offset, 0.5);
+ assert_eq!(stops[2].offset, 0.8);
+}
+
+#[test]
+fn px_and_em_offsets_use_gradient_length() {
+ let stops = normalize_color_stops(
+ &[
+ stop(literal("red"), Some(Distance::Px("50".into()))),
+ stop(literal("blue"), Some(Distance::Em("6.25".into()))),
+ ],
+ 200.0,
+ );
+ assert_eq!(stops[0].offset, 0.25);
+ assert!((stops[1].offset - 0.5).abs() < 1e-9);
+}
+
+#[test]
+fn offsets_clamped() {
+ let stops = normalize_color_stops(
+ &[
+ stop(literal("red"), Some(Distance::Percent("-20".into()))),
+ stop(literal("blue"), Some(Distance::Percent("150".into()))),
+ ],
+ 100.0,
+ );
+ assert_eq!(stops[0].offset, 0.0);
+ assert_eq!(stops[1].offset, 1.0);
+}
+
+#[test]
+fn leading_unpositioned_stops() {
+ let stops = normalize_color_stops(
+ &[
+ stop(literal("a"), None),
+ stop(literal("b"), None),
+ stop(literal("c"), Some(Distance::Percent("90".into()))),
+ ],
+ 100.0,
+ );
+ assert_eq!(stops[0].offset, 0.0);
+ assert_eq!(stops[1].offset, 0.45);
+ assert_eq!(stops[2].offset, 0.9);
+}
+
+#[test]
+fn trailing_unpositioned_stops() {
+ let stops = normalize_color_stops(
+ &[
+ stop(literal("a"), Some(Distance::Percent("10".into()))),
+ stop(literal("b"), None),
+ stop(literal("c"), None),
+ ],
+ 100.0,
+ );
+ assert_eq!(stops[0].offset, 0.1);
+ assert!((stops[1].offset - 0.55).abs() < 1e-9);
+ assert_eq!(stops[2].offset, 1.0);
+}
+
+#[test]
+fn transparent_stop_adopts_previous_color() {
+ let stops = normalize_color_stops(
+ &[
+ stop(Color::Hex("ff0000".into()), None),
+ stop(literal("transparent"), None),
+ stop(Color::Hex("0000ff".into()), None),
+ ],
+ 100.0,
+ );
+ assert_eq!(stops[1].color, "rgba(255,0,0,0)");
+}
+
+#[test]
+fn transparent_stop_with_unparseable_donors_unchanged() {
+ let stops = normalize_color_stops(
+ &[
+ stop(literal("red"), None),
+ stop(literal("transparent"), None),
+ stop(literal("blue"), None),
+ ],
+ 100.0,
+ );
+ assert_eq!(stops[1].color, "transparent");
+}
+
+#[test]
+fn transparent_stop_adopts_next_color_when_first() {
+ let stops = normalize_color_stops(
+ &[
+ stop(literal("transparent"), None),
+ stop(Color::Hex("00ff00".into()), None),
+ ],
+ 100.0,
+ );
+ assert_eq!(stops[1].color, "#00ff00");
+ assert_eq!(stops[0].color, "rgba(0,255,0,0)");
+}
+
+#[test]
+fn all_transparent_stops_unchanged() {
+ let stops = normalize_color_stops(
+ &[
+ stop(literal("transparent"), None),
+ stop(Color::Rgba(vec!["0".into(), "0".into(), "0".into(), "0".into()]), None),
+ ],
+ 100.0,
+ );
+ assert_eq!(stops[0].color, "transparent");
+ assert_eq!(stops[1].color, "rgba(0,0,0,0)");
+}
diff --git a/rust/crates/graphics/tests/graphics_render.rs b/rust/crates/graphics/tests/graphics_render.rs
new file mode 100644
index 00000000..a72f7fc9
--- /dev/null
+++ b/rust/crates/graphics/tests/graphics_render.rs
@@ -0,0 +1,261 @@
+use graphics::color::{hsl_to_rgb, is_transparent, parse_color_to_rgb, parse_css_color};
+use graphics::{
+ GraphicsError, ParamValue, ParamValues, build_default_graphic_instance, draw_css_background,
+ get_graphic_definition, graphics_registry, parse_background_layers, render_graphic,
+ resolve_graphic_params, split_css_layers, BackgroundLayer, GradientKind,
+};
+use tiny_skia::Pixmap;
+
+fn pixel(pixmap: &Pixmap, x: u32, y: u32) -> (u8, u8, u8, u8) {
+ let px = pixmap.pixel(x, y).unwrap();
+ (px.red(), px.green(), px.blue(), px.alpha())
+}
+
+fn params(entries: &[(&str, ParamValue)]) -> ParamValues {
+ entries
+ .iter()
+ .map(|(key, value)| (key.to_string(), value.clone()))
+ .collect()
+}
+
+#[test]
+fn registry_lookup() {
+ let registry = graphics_registry();
+ assert!(registry.has("rectangle"));
+ assert!(registry.has("ellipse"));
+ assert!(registry.has("polygon"));
+ assert!(registry.has("star"));
+ assert_eq!(registry.get_all().len(), 4);
+
+ let rectangle = get_graphic_definition("rectangle").unwrap();
+ assert_eq!(rectangle.name, "Rectangle");
+ assert!(rectangle.keywords.contains(&"box"));
+
+ let err = match get_graphic_definition("nope") {
+ Ok(_) => panic!("expected unknown graphic error"),
+ Err(err) => err,
+ };
+ assert_eq!(err, GraphicsError::UnknownGraphic("nope".to_string()));
+ assert_eq!(err.to_string(), "Unknown graphic: nope");
+}
+
+#[test]
+fn default_instance_and_resolve() {
+ let instance = build_default_graphic_instance("polygon").unwrap();
+ assert_eq!(instance.definition_id, "polygon");
+ assert_eq!(instance.params.get("sides"), Some(&ParamValue::Number(5.0)));
+ assert_eq!(
+ instance.params.get("fill"),
+ Some(&ParamValue::String("#ffffff".to_string()))
+ );
+ assert_eq!(
+ instance.params.get("strokeAlign"),
+ Some(&ParamValue::String("center".to_string()))
+ );
+
+ let definition = get_graphic_definition("star").unwrap();
+ let resolved = resolve_graphic_params(
+ definition,
+ Some(&params(&[("depth", ParamValue::Number(30.0))])),
+ );
+ assert_eq!(resolved.get("depth"), Some(&ParamValue::Number(30.0)));
+ assert_eq!(resolved.get("points"), Some(&ParamValue::Number(5.0)));
+
+ let defaults = resolve_graphic_params(definition, None);
+ assert_eq!(defaults.get("depth"), Some(&ParamValue::Number(45.0)));
+}
+
+#[test]
+fn color_parsing() {
+ let red = parse_css_color("#ff0000").unwrap();
+ assert_eq!((red.r, red.g, red.b, red.a), (1.0, 0.0, 0.0, 1.0));
+ let short = parse_css_color("#f00").unwrap();
+ assert_eq!(short, red);
+ let with_alpha = parse_css_color("#ff000080").unwrap();
+ assert!((with_alpha.a - 128.0 / 255.0).abs() < 1e-9);
+ let named = parse_css_color("rebeccapurple").unwrap();
+ assert!((named.r - 102.0 / 255.0).abs() < 1e-9);
+ assert_eq!(parse_css_color("transparent").unwrap().a, 0.0);
+ let rgb = parse_css_color("rgb(255, 0, 0)").unwrap();
+ assert_eq!(rgb, red);
+ let rgb_percent = parse_css_color("rgb(100%, 0%, 0%)").unwrap();
+ assert!((rgb_percent.r - 1.0).abs() < 1e-9);
+ let rgba = parse_css_color("rgba(0, 255, 0, 0.5)").unwrap();
+ assert_eq!((rgba.g, rgba.a), (1.0, 0.5));
+ let hsl = parse_css_color("hsl(120, 100%, 50%)").unwrap();
+ assert!((hsl.g - 1.0).abs() < 1e-9 && hsl.r.abs() < 1e-9);
+ assert!(parse_css_color("not-a-color").is_none());
+
+ let (r, g, b) = hsl_to_rgb(0.0, 1.0, 0.5);
+ assert_eq!((r, g, b), (1.0, 0.0, 0.0));
+
+ assert!(is_transparent("transparent"));
+ assert!(is_transparent("rgba(1, 2, 3, 0)"));
+ assert!(!is_transparent("rgba(1, 2, 3, 0.5)"));
+ assert!(!is_transparent("#00000000"));
+
+ let parsed = parse_color_to_rgb("#ff0000").unwrap();
+ assert_eq!((parsed.r, parsed.g, parsed.b), (255.0, 0.0, 0.0));
+ assert!(parse_color_to_rgb("#ff00").is_none());
+ assert!(parse_color_to_rgb("hsl(0,100%,50%)").is_none());
+}
+
+#[test]
+fn layer_splitting() {
+ assert_eq!(
+ split_css_layers("#00ff00, linear-gradient(red, blue), rgba(1,2,3,0.5)"),
+ vec!["#00ff00", "linear-gradient(red, blue)", "rgba(1,2,3,0.5)"]
+ );
+
+ let layers = parse_background_layers("linear-gradient(red, blue)");
+ assert!(matches!(
+ &layers[0],
+ BackgroundLayer::Gradient(ast) if ast.kind == GradientKind::Linear
+ ));
+
+ let layers = parse_background_layers("rebeccapurple");
+ assert_eq!(layers, vec![BackgroundLayer::Color("rebeccapurple".to_string())]);
+}
+
+#[test]
+fn rectangle_render_regions() {
+ let pixmap = render_graphic("rectangle", None, 64, 64).unwrap();
+ assert_eq!(pixmap.width(), 64);
+ assert_eq!(pixmap.height(), 64);
+ assert_eq!(pixel(&pixmap, 32, 32), (255, 255, 255, 255));
+ assert_eq!(pixel(&pixmap, 0, 0), (255, 255, 255, 255));
+
+ let rounded = render_graphic(
+ "rectangle",
+ Some(&params(&[("cornerRadius", ParamValue::Number(50.0))])),
+ 64,
+ 64,
+ )
+ .unwrap();
+ assert_eq!(pixel(&rounded, 32, 32), (255, 255, 255, 255));
+ assert_eq!(pixel(&rounded, 0, 0), (0, 0, 0, 0));
+}
+
+#[test]
+fn ellipse_render_regions() {
+ let pixmap = render_graphic("ellipse", None, 64, 64).unwrap();
+ assert_eq!(pixel(&pixmap, 32, 32), (255, 255, 255, 255));
+ assert_eq!(pixel(&pixmap, 0, 0), (0, 0, 0, 0));
+ assert_eq!(pixel(&pixmap, 63, 63), (0, 0, 0, 0));
+}
+
+#[test]
+fn star_render_regions() {
+ let pixmap = render_graphic("star", None, 64, 64).unwrap();
+ assert_eq!(pixel(&pixmap, 32, 32), (255, 255, 255, 255));
+ assert_eq!(pixel(&pixmap, 0, 0), (0, 0, 0, 0));
+}
+
+#[test]
+fn polygon_render_regions() {
+ let pixmap = render_graphic(
+ "polygon",
+ Some(&params(&[("sides", ParamValue::Number(6.0))])),
+ 64,
+ 64,
+ )
+ .unwrap();
+ assert_eq!(pixel(&pixmap, 32, 32), (255, 255, 255, 255));
+ assert_eq!(pixel(&pixmap, 0, 0), (0, 0, 0, 0));
+}
+
+#[test]
+fn center_stroke_regions() {
+ let pixmap = render_graphic(
+ "rectangle",
+ Some(&params(&[
+ ("strokeWidth", ParamValue::Number(10.0)),
+ ("strokeAlign", ParamValue::String("center".to_string())),
+ ])),
+ 64,
+ 64,
+ )
+ .unwrap();
+ assert_eq!(pixel(&pixmap, 2, 32), (0, 0, 0, 255));
+ assert_eq!(pixel(&pixmap, 32, 32), (255, 255, 255, 255));
+}
+
+#[test]
+fn inside_stroke_regions() {
+ let pixmap = render_graphic(
+ "rectangle",
+ Some(&params(&[
+ ("strokeWidth", ParamValue::Number(10.0)),
+ ("strokeAlign", ParamValue::String("inside".to_string())),
+ ])),
+ 64,
+ 64,
+ )
+ .unwrap();
+ assert_eq!(pixel(&pixmap, 4, 32), (0, 0, 0, 255));
+ assert_eq!(pixel(&pixmap, 32, 32), (255, 255, 255, 255));
+}
+
+#[test]
+fn outside_stroke_has_no_inner_ring() {
+ let pixmap = render_graphic(
+ "rectangle",
+ Some(&params(&[
+ ("strokeWidth", ParamValue::Number(10.0)),
+ ("strokeAlign", ParamValue::String("outside".to_string())),
+ ])),
+ 64,
+ 64,
+ )
+ .unwrap();
+ assert_eq!(pixel(&pixmap, 12, 32), (255, 255, 255, 255));
+ assert_eq!(pixel(&pixmap, 32, 32), (255, 255, 255, 255));
+}
+
+#[test]
+fn invalid_fill_color_errors() {
+ let err = render_graphic(
+ "rectangle",
+ Some(&params(&[("fill", ParamValue::String("bogus".to_string()))])),
+ 8,
+ 8,
+ )
+ .unwrap_err();
+ assert!(matches!(err, GraphicsError::InvalidColor(_)));
+}
+
+#[test]
+fn gradient_background_linear() {
+ let mut pixmap = Pixmap::new(64, 64).unwrap();
+ draw_css_background(&mut pixmap, "linear-gradient(to right, #ff0000, #0000ff)").unwrap();
+ let left = pixel(&pixmap, 1, 32);
+ let right = pixel(&pixmap, 62, 32);
+ assert!(left.0 > 200 && left.2 < 60, "left was {left:?}");
+ assert!(right.2 > 200 && right.0 < 60, "right was {right:?}");
+ assert_eq!(left.3, 255);
+ assert_eq!(right.3, 255);
+}
+
+#[test]
+fn gradient_background_radial() {
+ let mut pixmap = Pixmap::new(64, 64).unwrap();
+ draw_css_background(&mut pixmap, "radial-gradient(circle, #ffffff, #000000)").unwrap();
+ let center = pixel(&pixmap, 32, 32);
+ assert!(center.0 > 200, "center was {center:?}");
+ let corner = pixel(&pixmap, 0, 0);
+ assert!(corner.0 < 60, "corner was {corner:?}");
+}
+
+#[test]
+fn background_layer_paint_order() {
+ let mut pixmap = Pixmap::new(16, 16).unwrap();
+ draw_css_background(&mut pixmap, "#00ff00, linear-gradient(#ff0000, #0000ff)").unwrap();
+ assert_eq!(pixel(&pixmap, 8, 8), (0, 255, 0, 255));
+
+ let mut pixmap = Pixmap::new(16, 16).unwrap();
+ draw_css_background(&mut pixmap, "rgba(255,0,0,0.5)").unwrap();
+ let px = pixel(&pixmap, 8, 8);
+ assert!(px.3 > 120 && px.3 < 135, "alpha was {px:?}");
+ assert!(px.0 > 120 && px.0 < 135, "premultiplied red was {px:?}");
+ assert_eq!(px.1, 0);
+}