summaryrefslogtreecommitdiffstats
path: root/servo/tests/unit
diff options
context:
space:
mode:
Diffstat (limited to 'servo/tests/unit')
-rw-r--r--servo/tests/unit/malloc_size_of/Cargo.toml13
-rw-r--r--servo/tests/unit/malloc_size_of/lib.rs105
-rw-r--r--servo/tests/unit/style/Cargo.toml30
-rw-r--r--servo/tests/unit/style/animated_properties.rs225
-rw-r--r--servo/tests/unit/style/attr.rs94
-rw-r--r--servo/tests/unit/style/custom_properties.rs49
-rw-r--r--servo/tests/unit/style/lib.rs35
-rw-r--r--servo/tests/unit/style/logical_geometry.rs81
-rw-r--r--servo/tests/unit/style/media_queries.rs578
-rw-r--r--servo/tests/unit/style/parsing/animation.rs21
-rw-r--r--servo/tests/unit/style/parsing/background.rs233
-rw-r--r--servo/tests/unit/style/parsing/border.rs219
-rw-r--r--servo/tests/unit/style/parsing/box_.rs22
-rw-r--r--servo/tests/unit/style/parsing/column.rs30
-rw-r--r--servo/tests/unit/style/parsing/effects.rs99
-rw-r--r--servo/tests/unit/style/parsing/image.rs154
-rw-r--r--servo/tests/unit/style/parsing/inherited_text.rs46
-rw-r--r--servo/tests/unit/style/parsing/mod.rs150
-rw-r--r--servo/tests/unit/style/parsing/outline.rs26
-rw-r--r--servo/tests/unit/style/parsing/position.rs145
-rw-r--r--servo/tests/unit/style/parsing/selectors.rs35
-rw-r--r--servo/tests/unit/style/parsing/supports.rs19
-rw-r--r--servo/tests/unit/style/parsing/text_overflow.rs30
-rw-r--r--servo/tests/unit/style/parsing/transition_duration.rs17
-rw-r--r--servo/tests/unit/style/parsing/transition_timing_function.rs62
-rw-r--r--servo/tests/unit/style/properties/mod.rs70
-rw-r--r--servo/tests/unit/style/properties/scaffolding.rs85
-rw-r--r--servo/tests/unit/style/properties/serialization.rs773
-rw-r--r--servo/tests/unit/style/rule_tree/bench.rs239
-rw-r--r--servo/tests/unit/style/rule_tree/mod.rs5
-rw-r--r--servo/tests/unit/style/size_of.rs51
-rw-r--r--servo/tests/unit/style/str.rs50
-rw-r--r--servo/tests/unit/style/stylesheets.rs564
-rw-r--r--servo/tests/unit/style/stylist.rs238
34 files changed, 4593 insertions, 0 deletions
diff --git a/servo/tests/unit/malloc_size_of/Cargo.toml b/servo/tests/unit/malloc_size_of/Cargo.toml
new file mode 100644
index 0000000000..80668d5ba6
--- /dev/null
+++ b/servo/tests/unit/malloc_size_of/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "malloc_size_of_tests"
+version = "0.0.1"
+authors = ["The Servo Project Developers"]
+license = "MPL-2.0"
+
+[lib]
+path = "lib.rs"
+test = false
+
+[dependencies]
+malloc_size_of = {path = "../../../components/malloc_size_of"}
+servo_arc = {path = "../../../components/servo_arc"}
diff --git a/servo/tests/unit/malloc_size_of/lib.rs b/servo/tests/unit/malloc_size_of/lib.rs
new file mode 100644
index 0000000000..1280d0a8f2
--- /dev/null
+++ b/servo/tests/unit/malloc_size_of/lib.rs
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+/**
+```
+extern crate malloc_size_of;
+extern crate servo_arc;
+
+fn sizeable<T: malloc_size_of::MallocSizeOf>() {}
+fn shallow_sizeable<T: malloc_size_of::MallocShallowSizeOf>() {}
+fn cloneable<T: Clone>() {}
+
+fn main() {
+ cloneable::<servo_arc::Arc<i32>>();
+ cloneable::<std::sync::Arc<i32>>();
+ cloneable::<std::rc::Rc<i32>>();
+}
+```
+*/
+pub fn imports_ok() {}
+
+pub mod does_not_impl_malloc_size_of {
+ /**
+ ```compile_fail,E0277
+ extern crate malloc_size_of;
+ extern crate servo_arc;
+
+ fn sizeable<T: malloc_size_of::MallocSizeOf>() {}
+
+ fn main() {
+ sizeable::<servo_arc::Arc<i32>>();
+ }
+ ```
+ */
+ pub fn servo_arc() {}
+
+ /**
+ ```compile_fail,E0277
+ extern crate malloc_size_of;
+
+ fn sizeable<T: malloc_size_of::MallocSizeOf>() {}
+
+ fn main() {
+ sizeable::<std::sync::Arc<i32>>();
+ }
+ ```
+ */
+ pub fn std_arc() {}
+
+ /**
+ ```compile_fail,E0277
+ extern crate malloc_size_of;
+
+ fn sizeable<T: malloc_size_of::MallocSizeOf>() {}
+
+ fn main() {
+ sizeable::<std::rc::Rc<i32>>();
+ }
+ ```
+ */
+ pub fn rc() {}
+}
+
+pub mod does_not_impl_malloc_shallow_size_of {
+ /**
+ ```compile_fail,E0277
+ extern crate malloc_size_of;
+ extern crate servo_arc;
+
+ fn shallow_sizeable<T: malloc_size_of::MallocShallowSizeOf>() {}
+
+ fn main() {
+ shallow_sizeable::<servo_arc::Arc<i32>>();
+ }
+ ```
+ */
+ pub fn servo_arc() {}
+
+ /**
+ ```compile_fail,E0277
+ extern crate malloc_size_of;
+
+ fn shallow_sizeable<T: malloc_size_of::MallocShallowSizeOf>() {}
+
+ fn main() {
+ shallow_sizeable::<std::sync::Arc<i32>>();
+ }
+ ```
+ */
+ pub fn std_arc() {}
+
+ /**
+ ```compile_fail,E0277
+ extern crate malloc_size_of;
+
+ fn shallow_sizeable<T: malloc_size_of::MallocShallowSizeOf>() {}
+
+ fn main() {
+ shallow_sizeable::<std::rc::Rc<i32>>();
+ }
+ ```
+ */
+ pub fn rc() {}
+}
diff --git a/servo/tests/unit/style/Cargo.toml b/servo/tests/unit/style/Cargo.toml
new file mode 100644
index 0000000000..3afe9f60c5
--- /dev/null
+++ b/servo/tests/unit/style/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "style_tests"
+version = "0.0.1"
+authors = ["The Servo Project Developers"]
+license = "MPL-2.0"
+
+[lib]
+name = "style_tests"
+path = "lib.rs"
+doctest = false
+
+[dependencies]
+byteorder = "1.0"
+app_units = "0.7"
+cssparser = "0.33"
+euclid = "0.22"
+html5ever = "0.22"
+parking_lot = "0.10"
+rayon = "1"
+serde_json = "1.0"
+selectors = {path = "../../../components/selectors"}
+servo_arc = {path = "../../../components/servo_arc"}
+servo_atoms = {path = "../../../components/atoms"}
+servo_config = {path = "../../../components/config"}
+servo_url = {path = "../../../components/url"}
+size_of_test = {path = "../../../components/size_of_test"}
+style = {path = "../../../components/style"}
+style_traits = {path = "../../../components/style_traits"}
+std_test_override = { path = "../../../components/std_test_override" }
+to_shmem = { path = "../../../components/to_shmem" }
diff --git a/servo/tests/unit/style/animated_properties.rs b/servo/tests/unit/style/animated_properties.rs
new file mode 100644
index 0000000000..05b74de16c
--- /dev/null
+++ b/servo/tests/unit/style/animated_properties.rs
@@ -0,0 +1,225 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use cssparser::RGBA;
+use style::values::animated::{Animate, Procedure, ToAnimatedValue};
+use style::values::computed::Percentage;
+use style::values::generics::transform::{Transform, TransformOperation};
+
+fn interpolate_rgba(from: RGBA, to: RGBA, progress: f64) -> RGBA {
+ let from = from.to_animated_value();
+ let to = to.to_animated_value();
+ RGBA::from_animated_value(
+ from.animate(&to, Procedure::Interpolate { progress })
+ .unwrap(),
+ )
+}
+
+// Color
+#[test]
+fn test_rgba_color_interepolation_preserves_transparent() {
+ assert_eq!(
+ interpolate_rgba(RGBA::transparent(), RGBA::transparent(), 0.5),
+ RGBA::transparent()
+ );
+}
+
+#[test]
+fn test_rgba_color_interepolation_alpha() {
+ assert_eq!(
+ interpolate_rgba(RGBA::new(200, 0, 0, 100), RGBA::new(0, 200, 0, 200), 0.5),
+ RGBA::new(67, 133, 0, 150)
+ );
+}
+
+#[test]
+fn test_rgba_color_interepolation_out_of_range_1() {
+ // Some cubic-bezier functions produce values that are out of range [0, 1].
+ // Unclamped cases.
+ assert_eq!(
+ interpolate_rgba(
+ RGBA::from_floats(0.3, 0.0, 0.0, 0.4),
+ RGBA::from_floats(0.0, 1.0, 0.0, 0.6),
+ -0.5
+ ),
+ RGBA::new(154, 0, 0, 77)
+ );
+}
+
+#[test]
+fn test_rgba_color_interepolation_out_of_range_2() {
+ assert_eq!(
+ interpolate_rgba(
+ RGBA::from_floats(1.0, 0.0, 0.0, 0.6),
+ RGBA::from_floats(0.0, 0.3, 0.0, 0.4),
+ 1.5
+ ),
+ RGBA::new(0, 154, 0, 77)
+ );
+}
+
+#[test]
+fn test_rgba_color_interepolation_out_of_range_clamped_1() {
+ assert_eq!(
+ interpolate_rgba(
+ RGBA::from_floats(1.0, 0.0, 0.0, 0.8),
+ RGBA::from_floats(0.0, 1.0, 0.0, 0.2),
+ -0.5
+ ),
+ RGBA::from_floats(1.0, 0.0, 0.0, 1.0)
+ );
+}
+
+#[test]
+fn test_rgba_color_interepolation_out_of_range_clamped_2() {
+ assert_eq!(
+ interpolate_rgba(
+ RGBA::from_floats(1.0, 0.0, 0.0, 0.8),
+ RGBA::from_floats(0.0, 1.0, 0.0, 0.2),
+ 1.5
+ ),
+ RGBA::from_floats(0.0, 0.0, 0.0, 0.0)
+ );
+}
+
+// Transform
+#[test]
+fn test_transform_interpolation_on_translate() {
+ use style::values::computed::{CalcLengthPercentage, Length, LengthPercentage};
+
+ let from = Transform(vec![TransformOperation::Translate3D(
+ LengthPercentage::Length(Length::new(0.)),
+ LengthPercentage::Length(Length::new(100.)),
+ Length::new(25.),
+ )]);
+ let to = Transform(vec![TransformOperation::Translate3D(
+ LengthPercentage::Length(Length::new(100.)),
+ LengthPercentage::Length(Length::new(0.)),
+ Length::new(75.),
+ )]);
+ assert_eq!(
+ from.animate(&to, Procedure::Interpolate { progress: 0.5 })
+ .unwrap(),
+ Transform(vec![TransformOperation::Translate3D(
+ LengthPercentage::Length(Length::new(50.)),
+ LengthPercentage::Length(Length::new(50.)),
+ Length::new(50.),
+ )])
+ );
+
+ let from = Transform(vec![TransformOperation::Translate3D(
+ LengthPercentage::Percentage(Percentage(0.5)),
+ LengthPercentage::Percentage(Percentage(1.0)),
+ Length::new(25.),
+ )]);
+ let to = Transform(vec![TransformOperation::Translate3D(
+ LengthPercentage::Length(Length::new(100.)),
+ LengthPercentage::Length(Length::new(50.)),
+ Length::new(75.),
+ )]);
+ assert_eq!(
+ from.animate(&to, Procedure::Interpolate { progress: 0.5 })
+ .unwrap(),
+ Transform(vec![TransformOperation::Translate3D(
+ // calc(50px + 25%)
+ LengthPercentage::Calc(CalcLengthPercentage::new(
+ Length::new(50.),
+ Some(Percentage(0.25))
+ )),
+ // calc(25px + 50%)
+ LengthPercentage::Calc(CalcLengthPercentage::new(
+ Length::new(25.),
+ Some(Percentage(0.5))
+ )),
+ Length::new(50.),
+ )])
+ );
+}
+
+#[test]
+fn test_transform_interpolation_on_scale() {
+ let from = Transform(vec![TransformOperation::Scale3D(1.0, 2.0, 1.0)]);
+ let to = Transform(vec![TransformOperation::Scale3D(2.0, 4.0, 2.0)]);
+ assert_eq!(
+ from.animate(&to, Procedure::Interpolate { progress: 0.5 })
+ .unwrap(),
+ Transform(vec![TransformOperation::Scale3D(1.5, 3.0, 1.5)])
+ );
+}
+
+#[test]
+fn test_transform_interpolation_on_rotate() {
+ use style::values::computed::Angle;
+
+ let from = Transform(vec![TransformOperation::Rotate3D(
+ 0.0,
+ 0.0,
+ 1.0,
+ Angle::from_radians(0.0),
+ )]);
+ let to = Transform(vec![TransformOperation::Rotate3D(
+ 0.0,
+ 0.0,
+ 1.0,
+ Angle::from_radians(100.0),
+ )]);
+ assert_eq!(
+ from.animate(&to, Procedure::Interpolate { progress: 0.5 })
+ .unwrap(),
+ Transform(vec![TransformOperation::Rotate3D(
+ 0.0,
+ 0.0,
+ 1.0,
+ Angle::from_radians(50.0)
+ ),])
+ );
+}
+
+#[test]
+fn test_transform_interpolation_on_skew() {
+ use style::values::computed::Angle;
+
+ let from = Transform(vec![TransformOperation::Skew(
+ Angle::from_radians(0.0),
+ Some(Angle::from_radians(100.0)),
+ )]);
+ let to = Transform(vec![TransformOperation::Skew(
+ Angle::from_radians(100.0),
+ Some(Angle::from_radians(0.0)),
+ )]);
+ assert_eq!(
+ from.animate(&to, Procedure::Interpolate { progress: 0.5 })
+ .unwrap(),
+ Transform(vec![TransformOperation::Skew(
+ Angle::from_radians(50.0),
+ Some(Angle::from_radians(50.0)),
+ )])
+ );
+}
+
+#[test]
+fn test_transform_interpolation_on_mismatched_lists() {
+ use style::values::computed::{Angle, Length, LengthPercentage};
+
+ let from = Transform(vec![TransformOperation::Rotate3D(
+ 0.0,
+ 0.0,
+ 1.0,
+ Angle::from_radians(100.0),
+ )]);
+ let to = Transform(vec![TransformOperation::Translate3D(
+ LengthPercentage::Length(Length::new(100.)),
+ LengthPercentage::Length(Length::new(0.)),
+ Length::new(0.),
+ )]);
+ assert_eq!(
+ from.animate(&to, Procedure::Interpolate { progress: 0.5 })
+ .unwrap(),
+ Transform(vec![TransformOperation::InterpolateMatrix {
+ from_list: from.clone(),
+ to_list: to.clone(),
+ progress: Percentage(0.5),
+ }])
+ );
+}
diff --git a/servo/tests/unit/style/attr.rs b/servo/tests/unit/style/attr.rs
new file mode 100644
index 0000000000..aae30c7dcf
--- /dev/null
+++ b/servo/tests/unit/style/attr.rs
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use app_units::Au;
+use style::attr::{parse_length, AttrValue, LengthPercentageOrAuto};
+use style::values::computed::{CalcLengthPercentage, Percentage};
+
+#[test]
+fn test_length_calc() {
+ let calc = CalcLengthPercentage::new(Au(10).into(), Some(Percentage(0.2)));
+ assert_eq!(calc.to_used_value(Some(Au(10))), Some(Au(12)));
+ assert_eq!(calc.to_used_value(Some(Au(0))), Some(Au(10)));
+ assert_eq!(calc.to_used_value(None), None);
+
+ let calc = CalcLengthPercentage::new(Au(10).into(), None);
+ assert_eq!(calc.to_used_value(Some(Au(0))), Some(Au(10)));
+ assert_eq!(calc.to_used_value(None), Some(Au(10)));
+}
+
+#[test]
+fn test_parse_double() {
+ let value = String::from("432.5e2");
+ match AttrValue::from_double(value, 0.0) {
+ AttrValue::Double(_, num) => assert_eq!(num, 43250f64),
+ _ => panic!("expected a double value"),
+ }
+}
+
+#[test]
+fn test_parse_double_negative_prefix() {
+ let value = String::from("-5.6");
+ match AttrValue::from_double(value, 0.0) {
+ AttrValue::Double(_, num) => assert_eq!(num, -5.6f64),
+ _ => panic!("expected a double value"),
+ }
+}
+
+#[test]
+fn test_parse_double_positive_prefix() {
+ let value = String::from("+5.6");
+ match AttrValue::from_double(value, 0.0) {
+ AttrValue::Double(_, num) => assert_eq!(num, 5.6f64),
+ _ => panic!("expected a double value"),
+ }
+}
+
+#[test]
+fn test_from_limited_i32_should_be_default_when_less_than_0() {
+ let value = String::from("-1");
+ match AttrValue::from_limited_i32(value, 0) {
+ AttrValue::Int(_, 0) => (),
+ _ => panic!("expected an IndexSize error"),
+ }
+}
+
+#[test]
+fn test_from_limited_i32_should_parse_a_uint_when_value_is_0_or_greater() {
+ match AttrValue::from_limited_i32(String::from("1"), 0) {
+ AttrValue::Int(_, 1) => (),
+ _ => panic!("expected an successful parsing"),
+ }
+}
+
+#[test]
+fn test_from_limited_i32_should_keep_parsed_value_when_not_an_int() {
+ match AttrValue::from_limited_i32(String::from("parsed-value"), 0) {
+ AttrValue::Int(p, 0) => {
+ assert_eq!(p, String::from("parsed-value"))
+ },
+ _ => panic!("expected an successful parsing"),
+ }
+}
+
+#[test]
+pub fn test_parse_length() {
+ fn check(input: &str, expected: LengthPercentageOrAuto) {
+ let parsed = parse_length(input);
+ assert_eq!(parsed, expected);
+ }
+
+ check("0", LengthPercentageOrAuto::Length(Au::from_px(0)));
+ check("0.000%", LengthPercentageOrAuto::Percentage(0.0));
+ check("+5.82%", LengthPercentageOrAuto::Percentage(0.0582));
+ check(
+ "5.82",
+ LengthPercentageOrAuto::Length(Au::from_f64_px(5.82)),
+ );
+ check("invalid", LengthPercentageOrAuto::Auto);
+ check(
+ "12 followed by invalid",
+ LengthPercentageOrAuto::Length(Au::from_px(12)),
+ );
+}
diff --git a/servo/tests/unit/style/custom_properties.rs b/servo/tests/unit/style/custom_properties.rs
new file mode 100644
index 0000000000..b24ca3b132
--- /dev/null
+++ b/servo/tests/unit/style/custom_properties.rs
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use cssparser::{Parser, ParserInput};
+use servo_arc::Arc;
+use style::custom_properties::{
+ CssEnvironment, CustomPropertiesBuilder, CustomPropertiesMap, Name, SpecifiedValue,
+};
+use style::properties::CustomDeclarationValue;
+use test::{self, Bencher};
+
+fn cascade(
+ name_and_value: &[(&str, &str)],
+ inherited: Option<&Arc<CustomPropertiesMap>>,
+) -> Option<Arc<CustomPropertiesMap>> {
+ let values = name_and_value
+ .iter()
+ .map(|&(name, value)| {
+ let mut input = ParserInput::new(value);
+ let mut parser = Parser::new(&mut input);
+ (
+ Name::from(name),
+ SpecifiedValue::parse(&mut parser).unwrap(),
+ )
+ })
+ .collect::<Vec<_>>();
+
+ let env = CssEnvironment;
+ let mut builder = CustomPropertiesBuilder::new(inherited, &env);
+
+ for &(ref name, ref val) in &values {
+ builder.cascade(name, &CustomDeclarationValue::Value(val.clone()));
+ }
+
+ builder.build()
+}
+
+#[bench]
+fn cascade_custom_simple(b: &mut Bencher) {
+ b.iter(|| {
+ let parent = cascade(&[("foo", "10px"), ("bar", "100px")], None);
+
+ test::black_box(cascade(
+ &[("baz", "calc(40em + 4px)"), ("bazz", "calc(30em + 4px)")],
+ parent.as_ref(),
+ ))
+ })
+}
diff --git a/servo/tests/unit/style/lib.rs b/servo/tests/unit/style/lib.rs
new file mode 100644
index 0000000000..998cb9534d
--- /dev/null
+++ b/servo/tests/unit/style/lib.rs
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#![cfg(test)]
+#![feature(test)]
+
+extern crate app_units;
+extern crate cssparser;
+extern crate euclid;
+#[macro_use] extern crate html5ever;
+extern crate parking_lot;
+extern crate rayon;
+extern crate selectors;
+extern crate serde_json;
+extern crate servo_arc;
+extern crate servo_atoms;
+extern crate servo_config;
+extern crate servo_url;
+#[macro_use] extern crate style;
+extern crate style_traits;
+extern crate test;
+
+mod animated_properties;
+mod attr;
+mod custom_properties;
+mod logical_geometry;
+mod parsing;
+mod properties;
+mod rule_tree;
+mod size_of;
+mod specified_values;
+mod str;
+mod stylesheets;
+mod stylist;
diff --git a/servo/tests/unit/style/logical_geometry.rs b/servo/tests/unit/style/logical_geometry.rs
new file mode 100644
index 0000000000..235e3fb316
--- /dev/null
+++ b/servo/tests/unit/style/logical_geometry.rs
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use euclid::{Point2D, Rect, SideOffsets2D, Size2D};
+use style::logical_geometry::{LogicalMargin, LogicalPoint, LogicalRect, LogicalSize, WritingMode};
+
+#[cfg(test)]
+fn modes() -> Vec<WritingMode> {
+ vec![
+ WritingMode::empty(),
+ WritingMode::VERTICAL,
+ WritingMode::VERTICAL | WritingMode::VERTICAL_LR,
+ WritingMode::VERTICAL | WritingMode::VERTICAL_LR | WritingMode::VERTICAL_SIDEWAYS,
+ WritingMode::VERTICAL | WritingMode::VERTICAL_LR | WritingMode::TEXT_SIDEWAYS,
+ WritingMode::VERTICAL | WritingMode::VERTICAL_SIDEWAYS,
+ WritingMode::VERTICAL | WritingMode::TEXT_SIDEWAYS,
+ WritingMode::VERTICAL | WritingMode::UPRIGHT,
+ WritingMode::RTL,
+ WritingMode::VERTICAL | WritingMode::RTL,
+ WritingMode::VERTICAL | WritingMode::VERTICAL_LR | WritingMode::RTL,
+ WritingMode::VERTICAL |
+ WritingMode::VERTICAL_LR |
+ WritingMode::VERTICAL_SIDEWAYS |
+ WritingMode::RTL,
+ WritingMode::VERTICAL |
+ WritingMode::VERTICAL_LR |
+ WritingMode::TEXT_SIDEWAYS |
+ WritingMode::RTL,
+ WritingMode::VERTICAL | WritingMode::VERTICAL_LR | WritingMode::UPRIGHT | WritingMode::RTL,
+ WritingMode::VERTICAL | WritingMode::VERTICAL_SIDEWAYS | WritingMode::RTL,
+ WritingMode::VERTICAL | WritingMode::TEXT_SIDEWAYS | WritingMode::RTL,
+ WritingMode::VERTICAL | WritingMode::UPRIGHT | WritingMode::RTL,
+ ]
+}
+
+#[test]
+fn test_size_round_trip() {
+ let physical = Size2D::new(1u32, 2u32);
+ for &mode in modes().iter() {
+ let logical = LogicalSize::from_physical(mode, physical);
+ assert_eq!(logical.to_physical(mode), physical);
+ assert_eq!(logical.width(mode), 1);
+ assert_eq!(logical.height(mode), 2);
+ }
+}
+
+#[test]
+fn test_point_round_trip() {
+ let physical = Point2D::new(1u32, 2u32);
+ let container = Size2D::new(100, 200);
+ for &mode in modes().iter() {
+ let logical = LogicalPoint::from_physical(mode, physical, container);
+ assert_eq!(logical.to_physical(mode, container), physical);
+ assert_eq!(logical.x(mode, container), 1);
+ assert_eq!(logical.y(mode, container), 2);
+ }
+}
+
+#[test]
+fn test_margin_round_trip() {
+ let physical = SideOffsets2D::new(1u32, 2u32, 3u32, 4u32);
+ for &mode in modes().iter() {
+ let logical = LogicalMargin::from_physical(mode, physical);
+ assert_eq!(logical.to_physical(mode), physical);
+ assert_eq!(logical.top(mode), 1);
+ assert_eq!(logical.right(mode), 2);
+ assert_eq!(logical.bottom(mode), 3);
+ assert_eq!(logical.left(mode), 4);
+ }
+}
+
+#[test]
+fn test_rect_round_trip() {
+ let physical = Rect::new(Point2D::new(1u32, 2u32), Size2D::new(3u32, 4u32));
+ let container = Size2D::new(100, 200);
+ for &mode in modes().iter() {
+ let logical = LogicalRect::from_physical(mode, physical, container);
+ assert_eq!(logical.to_physical(mode, container), physical);
+ }
+}
diff --git a/servo/tests/unit/style/media_queries.rs b/servo/tests/unit/style/media_queries.rs
new file mode 100644
index 0000000000..e2d5c77b48
--- /dev/null
+++ b/servo/tests/unit/style/media_queries.rs
@@ -0,0 +1,578 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use euclid::Scale;
+use euclid::Size2D;
+use servo_arc::Arc;
+use servo_url::ServoUrl;
+use std::borrow::ToOwned;
+use style::context::QuirksMode;
+use style::media_queries::*;
+use style::servo::media_queries::*;
+use style::shared_lock::SharedRwLock;
+use style::stylesheets::{AllRules, CssRule, Origin, Stylesheet, StylesheetInDocument};
+use style::values::{specified, CustomIdent};
+use style::Atom;
+use style_traits::ToCss;
+
+fn test_media_rule<F>(css: &str, callback: F)
+where
+ F: Fn(&MediaList, &str),
+{
+ let url = ServoUrl::parse("http://localhost").unwrap();
+ let css_str = css.to_owned();
+ let lock = SharedRwLock::new();
+ let media_list = Arc::new(lock.wrap(MediaList::empty()));
+ let stylesheet = Stylesheet::from_str(
+ css,
+ url,
+ Origin::Author,
+ media_list,
+ lock,
+ None,
+ None,
+ QuirksMode::NoQuirks,
+ 0,
+ );
+ let dummy = Device::new(
+ MediaType::screen(),
+ Size2D::new(200.0, 100.0),
+ Scale::new(1.0),
+ );
+ let mut rule_count = 0;
+ let guard = stylesheet.shared_lock.read();
+ for rule in stylesheet.iter_rules::<AllRules>(&dummy, &guard) {
+ if let CssRule::Media(ref lock) = *rule {
+ rule_count += 1;
+ callback(&lock.read_with(&guard).media_queries.read_with(&guard), css);
+ }
+ }
+ assert!(rule_count > 0, css_str);
+}
+
+fn media_query_test(device: &Device, css: &str, expected_rule_count: usize) {
+ let url = ServoUrl::parse("http://localhost").unwrap();
+ let lock = SharedRwLock::new();
+ let media_list = Arc::new(lock.wrap(MediaList::empty()));
+ let ss = Stylesheet::from_str(
+ css,
+ url,
+ Origin::Author,
+ media_list,
+ lock,
+ None,
+ None,
+ QuirksMode::NoQuirks,
+ 0,
+ );
+ let mut rule_count = 0;
+ ss.effective_style_rules(device, &ss.shared_lock.read(), |_| rule_count += 1);
+ assert!(rule_count == expected_rule_count, css.to_owned());
+}
+
+#[test]
+fn test_mq_empty() {
+ test_media_rule("@media { }", |list, css| {
+ assert!(list.media_queries.len() == 0, css.to_owned());
+ });
+}
+
+#[test]
+fn test_mq_screen() {
+ test_media_rule("@media screen { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == None, css.to_owned());
+ assert!(
+ q.media_type == MediaQueryType::Concrete(MediaType::screen()),
+ css.to_owned()
+ );
+ assert!(q.expressions.len() == 0, css.to_owned());
+ });
+
+ test_media_rule("@media only screen { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == Some(Qualifier::Only), css.to_owned());
+ assert!(
+ q.media_type == MediaQueryType::Concrete(MediaType::screen()),
+ css.to_owned()
+ );
+ assert!(q.expressions.len() == 0, css.to_owned());
+ });
+
+ test_media_rule("@media not screen { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == Some(Qualifier::Not), css.to_owned());
+ assert!(
+ q.media_type == MediaQueryType::Concrete(MediaType::screen()),
+ css.to_owned()
+ );
+ assert!(q.expressions.len() == 0, css.to_owned());
+ });
+}
+
+#[test]
+fn test_mq_print() {
+ test_media_rule("@media print { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == None, css.to_owned());
+ assert!(
+ q.media_type == MediaQueryType::Concrete(MediaType::print()),
+ css.to_owned()
+ );
+ assert!(q.expressions.len() == 0, css.to_owned());
+ });
+
+ test_media_rule("@media only print { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == Some(Qualifier::Only), css.to_owned());
+ assert!(
+ q.media_type == MediaQueryType::Concrete(MediaType::print()),
+ css.to_owned()
+ );
+ assert!(q.expressions.len() == 0, css.to_owned());
+ });
+
+ test_media_rule("@media not print { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == Some(Qualifier::Not), css.to_owned());
+ assert!(
+ q.media_type == MediaQueryType::Concrete(MediaType::print()),
+ css.to_owned()
+ );
+ assert!(q.expressions.len() == 0, css.to_owned());
+ });
+}
+
+#[test]
+fn test_mq_unknown() {
+ test_media_rule("@media fridge { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == None, css.to_owned());
+ assert!(
+ q.media_type == MediaQueryType::Concrete(MediaType(CustomIdent(Atom::from("fridge")))),
+ css.to_owned()
+ );
+ assert!(q.expressions.len() == 0, css.to_owned());
+ });
+
+ test_media_rule("@media only glass { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == Some(Qualifier::Only), css.to_owned());
+ assert!(
+ q.media_type == MediaQueryType::Concrete(MediaType(CustomIdent(Atom::from("glass")))),
+ css.to_owned()
+ );
+ assert!(q.expressions.len() == 0, css.to_owned());
+ });
+
+ test_media_rule("@media not wood { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == Some(Qualifier::Not), css.to_owned());
+ assert!(
+ q.media_type == MediaQueryType::Concrete(MediaType(CustomIdent(Atom::from("wood")))),
+ css.to_owned()
+ );
+ assert!(q.expressions.len() == 0, css.to_owned());
+ });
+}
+
+#[test]
+fn test_mq_all() {
+ test_media_rule("@media all { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == None, css.to_owned());
+ assert!(q.media_type == MediaQueryType::All, css.to_owned());
+ assert!(q.expressions.len() == 0, css.to_owned());
+ });
+
+ test_media_rule("@media only all { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == Some(Qualifier::Only), css.to_owned());
+ assert!(q.media_type == MediaQueryType::All, css.to_owned());
+ assert!(q.expressions.len() == 0, css.to_owned());
+ });
+
+ test_media_rule("@media not all { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == Some(Qualifier::Not), css.to_owned());
+ assert!(q.media_type == MediaQueryType::All, css.to_owned());
+ assert!(q.expressions.len() == 0, css.to_owned());
+ });
+}
+
+#[test]
+fn test_mq_or() {
+ test_media_rule("@media screen, print { }", |list, css| {
+ assert!(list.media_queries.len() == 2, css.to_owned());
+ let q0 = &list.media_queries[0];
+ assert!(q0.qualifier == None, css.to_owned());
+ assert!(
+ q0.media_type == MediaQueryType::Concrete(MediaType::screen()),
+ css.to_owned()
+ );
+ assert!(q0.expressions.len() == 0, css.to_owned());
+
+ let q1 = &list.media_queries[1];
+ assert!(q1.qualifier == None, css.to_owned());
+ assert!(
+ q1.media_type == MediaQueryType::Concrete(MediaType::print()),
+ css.to_owned()
+ );
+ assert!(q1.expressions.len() == 0, css.to_owned());
+ });
+}
+
+#[test]
+fn test_mq_default_expressions() {
+ test_media_rule("@media (min-width: 100px) { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == None, css.to_owned());
+ assert!(q.media_type == MediaQueryType::All, css.to_owned());
+ assert!(q.expressions.len() == 1, css.to_owned());
+ match *q.expressions[0].kind_for_testing() {
+ ExpressionKind::Width(Range::Min(ref w)) => {
+ assert!(*w == specified::Length::from_px(100.))
+ },
+ _ => panic!("wrong expression type"),
+ }
+ });
+
+ test_media_rule("@media (max-width: 43px) { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == None, css.to_owned());
+ assert!(q.media_type == MediaQueryType::All, css.to_owned());
+ assert!(q.expressions.len() == 1, css.to_owned());
+ match *q.expressions[0].kind_for_testing() {
+ ExpressionKind::Width(Range::Max(ref w)) => {
+ assert!(*w == specified::Length::from_px(43.))
+ },
+ _ => panic!("wrong expression type"),
+ }
+ });
+}
+
+#[test]
+fn test_mq_expressions() {
+ test_media_rule("@media screen and (min-width: 100px) { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == None, css.to_owned());
+ assert!(
+ q.media_type == MediaQueryType::Concrete(MediaType::screen()),
+ css.to_owned()
+ );
+ assert!(q.expressions.len() == 1, css.to_owned());
+ match *q.expressions[0].kind_for_testing() {
+ ExpressionKind::Width(Range::Min(ref w)) => {
+ assert!(*w == specified::Length::from_px(100.))
+ },
+ _ => panic!("wrong expression type"),
+ }
+ });
+
+ test_media_rule("@media print and (max-width: 43px) { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == None, css.to_owned());
+ assert!(
+ q.media_type == MediaQueryType::Concrete(MediaType::print()),
+ css.to_owned()
+ );
+ assert!(q.expressions.len() == 1, css.to_owned());
+ match *q.expressions[0].kind_for_testing() {
+ ExpressionKind::Width(Range::Max(ref w)) => {
+ assert!(*w == specified::Length::from_px(43.))
+ },
+ _ => panic!("wrong expression type"),
+ }
+ });
+
+ test_media_rule("@media print and (width: 43px) { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == None, css.to_owned());
+ assert!(
+ q.media_type == MediaQueryType::Concrete(MediaType::print()),
+ css.to_owned()
+ );
+ assert!(q.expressions.len() == 1, css.to_owned());
+ match *q.expressions[0].kind_for_testing() {
+ ExpressionKind::Width(Range::Eq(ref w)) => {
+ assert!(*w == specified::Length::from_px(43.))
+ },
+ _ => panic!("wrong expression type"),
+ }
+ });
+
+ test_media_rule("@media fridge and (max-width: 52px) { }", |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == None, css.to_owned());
+ assert!(
+ q.media_type == MediaQueryType::Concrete(MediaType(CustomIdent(Atom::from("fridge")))),
+ css.to_owned()
+ );
+ assert!(q.expressions.len() == 1, css.to_owned());
+ match *q.expressions[0].kind_for_testing() {
+ ExpressionKind::Width(Range::Max(ref w)) => {
+ assert!(*w == specified::Length::from_px(52.))
+ },
+ _ => panic!("wrong expression type"),
+ }
+ });
+}
+
+#[test]
+fn test_to_css() {
+ test_media_rule("@media print and (width: 43px) { }", |list, _| {
+ let q = &list.media_queries[0];
+ let dest = q.to_css_string();
+ assert_eq!(dest, "print and (width: 43px)");
+ });
+}
+
+#[test]
+fn test_mq_multiple_expressions() {
+ test_media_rule(
+ "@media (min-width: 100px) and (max-width: 200px) { }",
+ |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == None, css.to_owned());
+ assert!(q.media_type == MediaQueryType::All, css.to_owned());
+ assert!(q.expressions.len() == 2, css.to_owned());
+ match *q.expressions[0].kind_for_testing() {
+ ExpressionKind::Width(Range::Min(ref w)) => {
+ assert!(*w == specified::Length::from_px(100.))
+ },
+ _ => panic!("wrong expression type"),
+ }
+ match *q.expressions[1].kind_for_testing() {
+ ExpressionKind::Width(Range::Max(ref w)) => {
+ assert!(*w == specified::Length::from_px(200.))
+ },
+ _ => panic!("wrong expression type"),
+ }
+ },
+ );
+
+ test_media_rule(
+ "@media not screen and (min-width: 100px) and (max-width: 200px) { }",
+ |list, css| {
+ assert!(list.media_queries.len() == 1, css.to_owned());
+ let q = &list.media_queries[0];
+ assert!(q.qualifier == Some(Qualifier::Not), css.to_owned());
+ assert!(
+ q.media_type == MediaQueryType::Concrete(MediaType::screen()),
+ css.to_owned()
+ );
+ assert!(q.expressions.len() == 2, css.to_owned());
+ match *q.expressions[0].kind_for_testing() {
+ ExpressionKind::Width(Range::Min(ref w)) => {
+ assert!(*w == specified::Length::from_px(100.))
+ },
+ _ => panic!("wrong expression type"),
+ }
+ match *q.expressions[1].kind_for_testing() {
+ ExpressionKind::Width(Range::Max(ref w)) => {
+ assert!(*w == specified::Length::from_px(200.))
+ },
+ _ => panic!("wrong expression type"),
+ }
+ },
+ );
+}
+
+#[test]
+fn test_mq_malformed_expressions() {
+ fn check_malformed_expr(list: &MediaList, css: &str) {
+ assert!(!list.media_queries.is_empty(), css.to_owned());
+ for mq in &list.media_queries {
+ assert!(mq.qualifier == Some(Qualifier::Not), css.to_owned());
+ assert!(mq.media_type == MediaQueryType::All, css.to_owned());
+ assert!(mq.expressions.is_empty(), css.to_owned());
+ }
+ }
+
+ for rule in &[
+ "@media (min-width: 100blah) and (max-width: 200px) { }",
+ "@media screen and (height: 200px) { }",
+ "@media (min-width: 30em foo bar) {}",
+ "@media not {}",
+ "@media not (min-width: 300px) {}",
+ "@media , {}",
+ ] {
+ test_media_rule(rule, check_malformed_expr);
+ }
+}
+
+#[test]
+fn test_matching_simple() {
+ let device = Device::new(
+ MediaType::screen(),
+ Size2D::new(200.0, 100.0),
+ Scale::new(1.0),
+ );
+
+ media_query_test(&device, "@media not all { a { color: red; } }", 0);
+ media_query_test(&device, "@media not screen { a { color: red; } }", 0);
+ media_query_test(&device, "@media not print { a { color: red; } }", 1);
+
+ media_query_test(&device, "@media unknown { a { color: red; } }", 0);
+ media_query_test(&device, "@media not unknown { a { color: red; } }", 1);
+
+ media_query_test(&device, "@media { a { color: red; } }", 1);
+ media_query_test(&device, "@media screen { a { color: red; } }", 1);
+ media_query_test(&device, "@media print { a { color: red; } }", 0);
+}
+
+#[test]
+fn test_matching_width() {
+ let device = Device::new(
+ MediaType::screen(),
+ Size2D::new(200.0, 100.0),
+ Scale::new(1.0),
+ );
+
+ media_query_test(&device, "@media { a { color: red; } }", 1);
+
+ media_query_test(&device, "@media (min-width: 50px) { a { color: red; } }", 1);
+ media_query_test(
+ &device,
+ "@media (min-width: 150px) { a { color: red; } }",
+ 1,
+ );
+ media_query_test(
+ &device,
+ "@media (min-width: 300px) { a { color: red; } }",
+ 0,
+ );
+
+ media_query_test(
+ &device,
+ "@media screen and (min-width: 50px) { a { color: red; } }",
+ 1,
+ );
+ media_query_test(
+ &device,
+ "@media screen and (min-width: 150px) { a { color: red; } }",
+ 1,
+ );
+ media_query_test(
+ &device,
+ "@media screen and (min-width: 300px) { a { color: red; } }",
+ 0,
+ );
+
+ media_query_test(
+ &device,
+ "@media not screen and (min-width: 50px) { a { color: red; } }",
+ 0,
+ );
+ media_query_test(
+ &device,
+ "@media not screen and (min-width: 150px) { a { color: red; } }",
+ 0,
+ );
+ media_query_test(
+ &device,
+ "@media not screen and (min-width: 300px) { a { color: red; } }",
+ 1,
+ );
+
+ media_query_test(&device, "@media (max-width: 50px) { a { color: red; } }", 0);
+ media_query_test(
+ &device,
+ "@media (max-width: 150px) { a { color: red; } }",
+ 0,
+ );
+ media_query_test(
+ &device,
+ "@media (max-width: 300px) { a { color: red; } }",
+ 1,
+ );
+
+ media_query_test(
+ &device,
+ "@media screen and (min-width: 50px) and (max-width: 100px) { a { color: red; } }",
+ 0,
+ );
+ media_query_test(
+ &device,
+ "@media screen and (min-width: 250px) and (max-width: 300px) { a { color: red; } }",
+ 0,
+ );
+ media_query_test(
+ &device,
+ "@media screen and (min-width: 50px) and (max-width: 250px) { a { color: red; } }",
+ 1,
+ );
+
+ media_query_test(
+ &device,
+ "@media not screen and (min-width: 50px) and (max-width: 100px) { a { color: red; } }",
+ 1,
+ );
+ media_query_test(
+ &device,
+ "@media not screen and (min-width: 250px) and (max-width: 300px) { a { color: red; } }",
+ 1,
+ );
+ media_query_test(
+ &device,
+ "@media not screen and (min-width: 50px) and (max-width: 250px) { a { color: red; } }",
+ 0,
+ );
+
+ media_query_test(
+ &device,
+ "@media not screen and (min-width: 3.1em) and (max-width: 6em) { a { color: red; } }",
+ 1,
+ );
+ media_query_test(
+ &device,
+ "@media not screen and (min-width: 16em) and (max-width: 19.75em) { a { color: red; } }",
+ 1,
+ );
+ media_query_test(
+ &device,
+ "@media not screen and (min-width: 3em) and (max-width: 250px) { a { color: red; } }",
+ 0,
+ );
+}
+
+#[test]
+fn test_matching_invalid() {
+ let device = Device::new(
+ MediaType::screen(),
+ Size2D::new(200.0, 100.0),
+ Scale::new(1.0),
+ );
+
+ media_query_test(&device, "@media fridge { a { color: red; } }", 0);
+ media_query_test(
+ &device,
+ "@media screen and (height: 100px) { a { color: red; } }",
+ 0,
+ );
+ media_query_test(
+ &device,
+ "@media not print and (width: 100) { a { color: red; } }",
+ 0,
+ );
+}
diff --git a/servo/tests/unit/style/parsing/animation.rs b/servo/tests/unit/style/parsing/animation.rs
new file mode 100644
index 0000000000..1b7481c4af
--- /dev/null
+++ b/servo/tests/unit/style/parsing/animation.rs
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use parsing::parse;
+use servo_atoms::Atom;
+use style::parser::Parse;
+use style::properties::longhands::animation_name;
+use style::values::specified::AnimationIterationCount;
+use style::values::{CustomIdent, KeyframesName};
+use style_traits::ToCss;
+
+#[test]
+fn test_animation_iteration() {
+ assert_roundtrip_with_context!(AnimationIterationCount::parse, "0", "0");
+ assert_roundtrip_with_context!(AnimationIterationCount::parse, "0.1", "0.1");
+ assert_roundtrip_with_context!(AnimationIterationCount::parse, "infinite", "infinite");
+
+ // Negative numbers are invalid
+ assert!(parse(AnimationIterationCount::parse, "-1").is_err());
+}
diff --git a/servo/tests/unit/style/parsing/background.rs b/servo/tests/unit/style/parsing/background.rs
new file mode 100644
index 0000000000..55b1dbcd25
--- /dev/null
+++ b/servo/tests/unit/style/parsing/background.rs
@@ -0,0 +1,233 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use parsing::parse;
+use style::properties::longhands::background_size;
+use style::properties::longhands::{
+ background_attachment, background_clip, background_color, background_image,
+};
+use style::properties::longhands::{
+ background_origin, background_position_x, background_position_y, background_repeat,
+};
+use style::properties::shorthands::background;
+
+#[test]
+fn background_shorthand_should_parse_all_available_properties_when_specified() {
+ let input = "url(\"http://servo/test.png\") top center / 200px 200px repeat-x fixed padding-box content-box red";
+ let result = parse(background::parse_value, input).unwrap();
+
+ assert_eq!(
+ result.background_image,
+ parse_longhand!(background_image, "url(\"http://servo/test.png\")")
+ );
+ assert_eq!(
+ result.background_position_x,
+ parse_longhand!(background_position_x, "center")
+ );
+ assert_eq!(
+ result.background_position_y,
+ parse_longhand!(background_position_y, "top")
+ );
+ assert_eq!(
+ result.background_size,
+ parse_longhand!(background_size, "200px 200px")
+ );
+ assert_eq!(
+ result.background_repeat,
+ parse_longhand!(background_repeat, "repeat-x")
+ );
+ assert_eq!(
+ result.background_attachment,
+ parse_longhand!(background_attachment, "fixed")
+ );
+ assert_eq!(
+ result.background_origin,
+ parse_longhand!(background_origin, "padding-box")
+ );
+ assert_eq!(
+ result.background_clip,
+ parse_longhand!(background_clip, "content-box")
+ );
+ assert_eq!(
+ result.background_color,
+ parse_longhand!(background_color, "red")
+ );
+}
+
+#[test]
+fn background_shorthand_should_parse_when_some_fields_set() {
+ let result = parse(background::parse_value, "14px 40px repeat-y").unwrap();
+
+ assert_eq!(
+ result.background_position_x,
+ parse_longhand!(background_position_x, "14px")
+ );
+ assert_eq!(
+ result.background_position_y,
+ parse_longhand!(background_position_y, "40px")
+ );
+ assert_eq!(
+ result.background_repeat,
+ parse_longhand!(background_repeat, "repeat-y")
+ );
+
+ let result = parse(
+ background::parse_value,
+ "url(\"http://servo/test.png\") repeat blue",
+ )
+ .unwrap();
+
+ assert_eq!(
+ result.background_image,
+ parse_longhand!(background_image, "url(\"http://servo/test.png\")")
+ );
+ assert_eq!(
+ result.background_repeat,
+ parse_longhand!(background_repeat, "repeat")
+ );
+ assert_eq!(
+ result.background_color,
+ parse_longhand!(background_color, "blue")
+ );
+
+ let result = parse(background::parse_value, "padding-box").unwrap();
+
+ assert_eq!(
+ result.background_origin,
+ parse_longhand!(background_origin, "padding-box")
+ );
+ assert_eq!(
+ result.background_clip,
+ parse_longhand!(background_clip, "padding-box")
+ );
+
+ let result = parse(background::parse_value, "url(\"http://servo/test.png\")").unwrap();
+
+ assert_eq!(
+ result.background_image,
+ parse_longhand!(background_image, "url(\"http://servo/test.png\")")
+ );
+}
+
+#[test]
+fn background_shorthand_should_parse_comma_separated_declarations() {
+ let input =
+ "url(\"http://servo/test.png\") top left no-repeat, url(\"http://servo/test.png\") \
+ center / 100% 100% no-repeat, white";
+ let result = parse(background::parse_value, input).unwrap();
+
+ assert_eq!(
+ result.background_image,
+ parse_longhand!(
+ background_image,
+ "url(\"http://servo/test.png\"), \
+ url(\"http://servo/test.png\"), none"
+ )
+ );
+ assert_eq!(
+ result.background_position_x,
+ parse_longhand!(background_position_x, "left, center, 0%")
+ );
+ assert_eq!(
+ result.background_position_y,
+ parse_longhand!(background_position_y, "top, center, 0%")
+ );
+ assert_eq!(
+ result.background_repeat,
+ parse_longhand!(background_repeat, "no-repeat, no-repeat, repeat")
+ );
+ assert_eq!(
+ result.background_clip,
+ parse_longhand!(background_clip, "border-box, border-box, border-box")
+ );
+ assert_eq!(
+ result.background_origin,
+ parse_longhand!(
+ background_origin,
+ "padding-box, padding-box, \
+ padding-box"
+ )
+ );
+ assert_eq!(
+ result.background_size,
+ parse_longhand!(background_size, "auto auto, 100% 100%, auto auto")
+ );
+ assert_eq!(
+ result.background_attachment,
+ parse_longhand!(background_attachment, "scroll, scroll, scroll")
+ );
+ assert_eq!(
+ result.background_color,
+ parse_longhand!(background_color, "white")
+ );
+}
+
+#[test]
+fn background_shorthand_should_parse_position_and_size_correctly() {
+ let result = parse(background::parse_value, "7px 4px").unwrap();
+
+ assert_eq!(
+ result.background_position_x,
+ parse_longhand!(background_position_x, "7px")
+ );
+ assert_eq!(
+ result.background_position_y,
+ parse_longhand!(background_position_y, "4px")
+ );
+
+ let result = parse(background::parse_value, "7px 4px / 30px 20px").unwrap();
+
+ assert_eq!(
+ result.background_position_x,
+ parse_longhand!(background_position_x, "7px")
+ );
+ assert_eq!(
+ result.background_position_y,
+ parse_longhand!(background_position_y, "4px")
+ );
+ assert_eq!(
+ result.background_size,
+ parse_longhand!(background_size, "30px 20px")
+ );
+
+ assert!(parse(background::parse_value, "/ 30px 20px").is_err());
+
+ assert!(parse(background::parse_value, "repeat-x / 30px 20px").is_err());
+}
+
+#[test]
+fn background_shorthand_should_parse_origin_and_clip_correctly() {
+ let result = parse(background::parse_value, "padding-box content-box").unwrap();
+
+ assert_eq!(
+ result.background_origin,
+ parse_longhand!(background_origin, "padding-box")
+ );
+ assert_eq!(
+ result.background_clip,
+ parse_longhand!(background_clip, "content-box")
+ );
+
+ let result = parse(background::parse_value, "padding-box padding-box").unwrap();
+
+ assert_eq!(
+ result.background_origin,
+ parse_longhand!(background_origin, "padding-box")
+ );
+ assert_eq!(
+ result.background_clip,
+ parse_longhand!(background_clip, "padding-box")
+ );
+
+ let result = parse(background::parse_value, "padding-box").unwrap();
+
+ assert_eq!(
+ result.background_origin,
+ parse_longhand!(background_origin, "padding-box")
+ );
+ assert_eq!(
+ result.background_clip,
+ parse_longhand!(background_clip, "padding-box")
+ );
+}
diff --git a/servo/tests/unit/style/parsing/border.rs b/servo/tests/unit/style/parsing/border.rs
new file mode 100644
index 0000000000..8e474ca542
--- /dev/null
+++ b/servo/tests/unit/style/parsing/border.rs
@@ -0,0 +1,219 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use parsing::parse;
+use style::parser::Parse;
+use style::properties::longhands::{border_image_outset, border_image_repeat, border_image_slice};
+use style::properties::longhands::{border_image_source, border_image_width};
+use style::properties::shorthands::border_image;
+use style::properties::MaybeBoxed;
+use style::values::specified::BorderRadius;
+use style_traits::ToCss;
+
+macro_rules! assert_longhand {
+ ($parsed_shorthand: expr, $prop: ident, $value_string: expr) => {
+ assert_eq!(
+ $parsed_shorthand.$prop,
+ parse_longhand!($prop, $value_string).maybe_boxed()
+ )
+ };
+}
+
+macro_rules! assert_initial {
+ ($parsed_shorthand: expr, $prop: ident) => {
+ assert_eq!(
+ $parsed_shorthand.$prop,
+ $prop::get_initial_specified_value().maybe_boxed()
+ )
+ };
+}
+
+macro_rules! assert_border_radius_values {
+ ($input:expr; $tlw:expr, $trw:expr, $brw:expr, $blw:expr ;
+ $tlh:expr, $trh:expr, $brh:expr, $blh:expr) => {
+ let input = parse(BorderRadius::parse, $input)
+ .expect(&format!("Failed parsing {} as border radius", $input));
+ assert_eq!(
+ ::style_traits::ToCss::to_css_string(&input.top_left.0.width()),
+ $tlw
+ );
+ assert_eq!(
+ ::style_traits::ToCss::to_css_string(&input.top_right.0.width()),
+ $trw
+ );
+ assert_eq!(
+ ::style_traits::ToCss::to_css_string(&input.bottom_right.0.width()),
+ $brw
+ );
+ assert_eq!(
+ ::style_traits::ToCss::to_css_string(&input.bottom_left.0.width()),
+ $blw
+ );
+ assert_eq!(
+ ::style_traits::ToCss::to_css_string(&input.top_left.0.height()),
+ $tlh
+ );
+ assert_eq!(
+ ::style_traits::ToCss::to_css_string(&input.top_right.0.height()),
+ $trh
+ );
+ assert_eq!(
+ ::style_traits::ToCss::to_css_string(&input.bottom_right.0.height()),
+ $brh
+ );
+ assert_eq!(
+ ::style_traits::ToCss::to_css_string(&input.bottom_left.0.height()),
+ $blh
+ );
+ };
+}
+
+#[test]
+fn test_border_radius() {
+ assert_border_radius_values!("10px";
+ "10px", "10px", "10px", "10px" ;
+ "10px", "10px", "10px", "10px");
+ assert_border_radius_values!("10px 20px";
+ "10px", "20px", "10px", "20px" ;
+ "10px", "20px", "10px", "20px");
+ assert_border_radius_values!("10px 20px 30px";
+ "10px", "20px", "30px", "20px" ;
+ "10px", "20px", "30px", "20px");
+ assert_border_radius_values!("10px 20px 30px 40px";
+ "10px", "20px", "30px", "40px" ;
+ "10px", "20px", "30px", "40px");
+ assert_border_radius_values!("10% / 20px";
+ "10%", "10%", "10%", "10%" ;
+ "20px", "20px", "20px", "20px");
+ assert_border_radius_values!("10px / 20px 30px";
+ "10px", "10px", "10px", "10px" ;
+ "20px", "30px", "20px", "30px");
+ assert_border_radius_values!("10px 20px 30px 40px / 1px 2px 3px 4px";
+ "10px", "20px", "30px", "40px" ;
+ "1px", "2px", "3px", "4px");
+ assert_border_radius_values!("10px 20px 30px 40px / 1px 2px 3px 4px";
+ "10px", "20px", "30px", "40px" ;
+ "1px", "2px", "3px", "4px");
+ assert_border_radius_values!("10px 20px 30px 40px / 1px 2px 3px 4px";
+ "10px", "20px", "30px", "40px" ;
+ "1px", "2px", "3px", "4px");
+ assert_border_radius_values!("10px -20px 30px 40px";
+ "10px", "10px", "10px", "10px";
+ "10px", "10px", "10px", "10px");
+ assert_border_radius_values!("10px 20px -30px 40px";
+ "10px", "20px", "10px", "20px";
+ "10px", "20px", "10px", "20px");
+ assert_border_radius_values!("10px 20px 30px -40px";
+ "10px", "20px", "30px", "20px";
+ "10px", "20px", "30px", "20px");
+ assert!(parse(BorderRadius::parse, "-10px 20px 30px 40px").is_err());
+}
+
+#[test]
+fn border_image_shorthand_should_parse_when_all_properties_specified() {
+ let input = "linear-gradient(red, blue) 30 30% 45 fill / 20px 40px / 10px round stretch";
+ let result = parse(border_image::parse_value, input).unwrap();
+
+ assert_longhand!(result, border_image_source, "linear-gradient(red, blue)");
+ assert_longhand!(result, border_image_slice, "30 30% 45 fill");
+ assert_longhand!(result, border_image_width, "20px 40px");
+ assert_longhand!(result, border_image_outset, "10px");
+ assert_longhand!(result, border_image_repeat, "round stretch");
+}
+
+#[test]
+fn border_image_shorthand_should_parse_without_width() {
+ let input = "linear-gradient(red, blue) 30 30% 45 fill / / 10px round stretch";
+ let result = parse(border_image::parse_value, input).unwrap();
+
+ assert_longhand!(result, border_image_source, "linear-gradient(red, blue)");
+ assert_longhand!(result, border_image_slice, "30 30% 45 fill");
+ assert_longhand!(result, border_image_outset, "10px");
+ assert_longhand!(result, border_image_repeat, "round stretch");
+ assert_initial!(result, border_image_width);
+}
+
+#[test]
+fn border_image_shorthand_should_parse_without_outset() {
+ let input = "linear-gradient(red, blue) 30 30% 45 fill / 20px 40px round";
+ let result = parse(border_image::parse_value, input).unwrap();
+
+ assert_longhand!(result, border_image_source, "linear-gradient(red, blue)");
+ assert_longhand!(result, border_image_slice, "30 30% 45 fill");
+ assert_longhand!(result, border_image_width, "20px 40px");
+ assert_longhand!(result, border_image_repeat, "round");
+ assert_initial!(result, border_image_outset);
+}
+
+#[test]
+fn border_image_shorthand_should_parse_without_width_or_outset() {
+ let input = "linear-gradient(red, blue) 30 30% 45 fill round";
+ let result = parse(border_image::parse_value, input).unwrap();
+
+ assert_longhand!(result, border_image_source, "linear-gradient(red, blue)");
+ assert_longhand!(result, border_image_slice, "30 30% 45 fill");
+ assert_longhand!(result, border_image_repeat, "round");
+ assert_initial!(result, border_image_width);
+ assert_initial!(result, border_image_outset);
+}
+
+#[test]
+fn border_image_shorthand_should_parse_with_just_source() {
+ let result = parse(border_image::parse_value, "linear-gradient(red, blue)").unwrap();
+
+ assert_longhand!(result, border_image_source, "linear-gradient(red, blue)");
+ assert_initial!(result, border_image_slice);
+ assert_initial!(result, border_image_width);
+ assert_initial!(result, border_image_outset);
+ assert_initial!(result, border_image_repeat);
+}
+
+#[test]
+fn border_image_outset_should_error_on_negative_length() {
+ let result = parse(border_image_outset::parse, "-1em");
+ assert!(result.is_err());
+}
+
+#[test]
+fn border_image_outset_should_error_on_negative_number() {
+ let result = parse(border_image_outset::parse, "-15");
+ assert!(result.is_err());
+}
+
+#[test]
+fn border_image_outset_should_return_number_on_plain_zero() {
+ let result = parse(border_image_outset::parse, "0");
+ assert_eq!(result.unwrap(), parse_longhand!(border_image_outset, "0"));
+}
+
+#[test]
+fn border_image_outset_should_return_length_on_length_zero() {
+ let result = parse(border_image_outset::parse, "0em");
+ assert_eq!(result.unwrap(), parse_longhand!(border_image_outset, "0em"));
+}
+
+#[test]
+fn test_border_style() {
+ use style::values::specified::BorderStyle;
+
+ assert_roundtrip_with_context!(<BorderStyle as Parse>::parse, r#"none"#);
+ assert_roundtrip_with_context!(<BorderStyle as Parse>::parse, r#"hidden"#);
+ assert_roundtrip_with_context!(<BorderStyle as Parse>::parse, r#"solid"#);
+ assert_roundtrip_with_context!(<BorderStyle as Parse>::parse, r#"double"#);
+ assert_roundtrip_with_context!(<BorderStyle as Parse>::parse, r#"dotted"#);
+ assert_roundtrip_with_context!(<BorderStyle as Parse>::parse, r#"dashed"#);
+ assert_roundtrip_with_context!(<BorderStyle as Parse>::parse, r#"groove"#);
+ assert_roundtrip_with_context!(<BorderStyle as Parse>::parse, r#"ridge"#);
+ assert_roundtrip_with_context!(<BorderStyle as Parse>::parse, r#"inset"#);
+ assert_roundtrip_with_context!(<BorderStyle as Parse>::parse, r#"outset"#);
+}
+
+#[test]
+fn test_border_spacing() {
+ use style::properties::longhands::border_spacing;
+
+ assert_parser_exhausted!(border_spacing::parse, "1px rubbish", false);
+ assert_parser_exhausted!(border_spacing::parse, "1px", true);
+ assert_parser_exhausted!(border_spacing::parse, "1px 2px", true);
+}
diff --git a/servo/tests/unit/style/parsing/box_.rs b/servo/tests/unit/style/parsing/box_.rs
new file mode 100644
index 0000000000..cec950c3cd
--- /dev/null
+++ b/servo/tests/unit/style/parsing/box_.rs
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use parsing::parse;
+use style_traits::ToCss;
+
+#[test]
+fn test_transform_translate() {
+ use style::properties::longhands::transform;
+ assert_roundtrip_with_context!(transform::parse, "translate(2px)");
+ assert_roundtrip_with_context!(transform::parse, "translate(2px, 5px)");
+ assert!(parse(transform::parse, "translate(2px foo)").is_err());
+ assert!(parse(transform::parse, "perspective(-10px)").is_err());
+}
+
+#[test]
+fn test_unexhausted_transform() {
+ use style::properties::longhands::transform;
+ assert_parser_exhausted!(transform::parse, "rotate(70deg)foo", false);
+ assert_parser_exhausted!(transform::parse, "rotate(70deg) foo", false);
+}
diff --git a/servo/tests/unit/style/parsing/column.rs b/servo/tests/unit/style/parsing/column.rs
new file mode 100644
index 0000000000..6e0e86182a
--- /dev/null
+++ b/servo/tests/unit/style/parsing/column.rs
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use parsing::parse;
+use style_traits::ToCss;
+
+#[test]
+fn test_column_width() {
+ use style::properties::longhands::column_width;
+
+ assert_roundtrip_with_context!(column_width::parse, "auto");
+ assert_roundtrip_with_context!(column_width::parse, "6px");
+ assert_roundtrip_with_context!(column_width::parse, "2.5em");
+ assert_roundtrip_with_context!(column_width::parse, "0.3vw");
+
+ assert!(parse(column_width::parse, "-6px").is_err());
+}
+
+#[test]
+fn test_column_gap() {
+ use style::properties::longhands::column_gap;
+
+ assert_roundtrip_with_context!(column_gap::parse, "normal");
+ assert_roundtrip_with_context!(column_gap::parse, "6px");
+ assert_roundtrip_with_context!(column_gap::parse, "2.5em");
+ assert_roundtrip_with_context!(column_gap::parse, "0.3vw");
+
+ assert!(parse(column_gap::parse, "-6px").is_err());
+}
diff --git a/servo/tests/unit/style/parsing/effects.rs b/servo/tests/unit/style/parsing/effects.rs
new file mode 100644
index 0000000000..1b377aca54
--- /dev/null
+++ b/servo/tests/unit/style/parsing/effects.rs
@@ -0,0 +1,99 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use parsing::parse;
+use style::properties::longhands::{perspective_origin, transform_origin};
+use style_traits::ToCss;
+
+#[test]
+fn test_clip() {
+ use style::properties::longhands::clip;
+
+ assert_roundtrip_with_context!(clip::parse, "auto");
+ assert_roundtrip_with_context!(clip::parse, "rect(1px, 2px, 3px, 4px)");
+ assert_roundtrip_with_context!(clip::parse, "rect(1px, auto, auto, 4px)");
+ assert_roundtrip_with_context!(clip::parse, "rect(auto, auto, auto, auto)");
+
+ // Non-standard syntax
+ assert_roundtrip_with_context!(
+ clip::parse,
+ "rect(1px 2px 3px 4px)",
+ "rect(1px, 2px, 3px, 4px)"
+ );
+ assert_roundtrip_with_context!(
+ clip::parse,
+ "rect(auto 2px 3px auto)",
+ "rect(auto, 2px, 3px, auto)"
+ );
+ assert_roundtrip_with_context!(
+ clip::parse,
+ "rect(1px auto auto 4px)",
+ "rect(1px, auto, auto, 4px)"
+ );
+ assert_roundtrip_with_context!(
+ clip::parse,
+ "rect(auto auto auto auto)",
+ "rect(auto, auto, auto, auto)"
+ );
+}
+
+#[test]
+fn test_effects_parser_exhaustion() {
+ assert_parser_exhausted!(perspective_origin::parse, "1px 1px", true);
+ assert_parser_exhausted!(transform_origin::parse, "1px 1px", true);
+
+ assert_parser_exhausted!(perspective_origin::parse, "1px some-rubbish", false);
+ assert_parser_exhausted!(transform_origin::parse, "1px some-rubbish", false);
+}
+
+#[test]
+fn test_parse_factor() {
+ use parsing::parse;
+ use style::properties::longhands::filter;
+
+ assert!(parse(filter::parse, "brightness(0)").is_ok());
+ assert!(parse(filter::parse, "brightness(55)").is_ok());
+ assert!(parse(filter::parse, "brightness(100)").is_ok());
+
+ assert!(parse(filter::parse, "contrast(0)").is_ok());
+ assert!(parse(filter::parse, "contrast(55)").is_ok());
+ assert!(parse(filter::parse, "contrast(100)").is_ok());
+
+ assert!(parse(filter::parse, "grayscale(0)").is_ok());
+ assert!(parse(filter::parse, "grayscale(55)").is_ok());
+ assert!(parse(filter::parse, "grayscale(100)").is_ok());
+
+ assert!(parse(filter::parse, "invert(0)").is_ok());
+ assert!(parse(filter::parse, "invert(55)").is_ok());
+ assert!(parse(filter::parse, "invert(100)").is_ok());
+
+ assert!(parse(filter::parse, "opacity(0)").is_ok());
+ assert!(parse(filter::parse, "opacity(55)").is_ok());
+ assert!(parse(filter::parse, "opacity(100)").is_ok());
+
+ assert!(parse(filter::parse, "sepia(0)").is_ok());
+ assert!(parse(filter::parse, "sepia(55)").is_ok());
+ assert!(parse(filter::parse, "sepia(100)").is_ok());
+
+ assert!(parse(filter::parse, "saturate(0)").is_ok());
+ assert!(parse(filter::parse, "saturate(55)").is_ok());
+ assert!(parse(filter::parse, "saturate(100)").is_ok());
+
+ // Negative numbers are invalid for certain filters
+ assert!(parse(filter::parse, "brightness(-1)").is_err());
+ assert!(parse(filter::parse, "contrast(-1)").is_err());
+ assert!(parse(filter::parse, "grayscale(-1)").is_err());
+ assert!(parse(filter::parse, "invert(-1)").is_err());
+ assert!(parse(filter::parse, "opacity(-1)").is_err());
+ assert!(parse(filter::parse, "sepia(-1)").is_err());
+ assert!(parse(filter::parse, "saturate(-1)").is_err());
+}
+
+#[test]
+fn blur_radius_should_not_accept_negavite_values() {
+ use style::properties::longhands::box_shadow;
+ assert!(parse(box_shadow::parse, "1px 1px -1px").is_err()); // for -ve values
+ assert!(parse(box_shadow::parse, "1px 1px 0").is_ok()); // for zero
+ assert!(parse(box_shadow::parse, "1px 1px 1px").is_ok()); // for +ve value
+}
diff --git a/servo/tests/unit/style/parsing/image.rs b/servo/tests/unit/style/parsing/image.rs
new file mode 100644
index 0000000000..33ccb88cb0
--- /dev/null
+++ b/servo/tests/unit/style/parsing/image.rs
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use style::parser::Parse;
+use style::values::specified::image::*;
+use style_traits::ToCss;
+
+#[test]
+fn test_linear_gradient() {
+ // Parsing from the right
+ assert_roundtrip_with_context!(Image::parse, "linear-gradient(to left, red, green)");
+
+ // Parsing from the left
+ assert_roundtrip_with_context!(Image::parse, "linear-gradient(to right, red, green)");
+
+ // Parsing with two values for <side-or-corner>
+ assert_roundtrip_with_context!(Image::parse, "linear-gradient(to right top, red, green)");
+
+ // Parsing with <angle>
+ assert_roundtrip_with_context!(Image::parse, "linear-gradient(45deg, red, green)");
+
+ // Parsing with more than two entries in <color-stop-list>
+ assert_roundtrip_with_context!(Image::parse, "linear-gradient(red, yellow, green)");
+
+ // Parsing with percentage in the <color-stop-list>
+ assert_roundtrip_with_context!(Image::parse, "linear-gradient(red, green, yellow 50%)");
+
+ // Parsing without <angle> and <side-or-corner>
+ assert_roundtrip_with_context!(Image::parse, "linear-gradient(red, green)");
+}
+
+#[test]
+fn test_radial_gradient() {
+ // Parsing with all values
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(circle closest-side at 20px 30px, red, green)"
+ );
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(ellipse closest-side at 20px 30px, red, green)",
+ "radial-gradient(closest-side at 20px 30px, red, green)"
+ );
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(closest-side circle at 20px 30px, red, green)",
+ "radial-gradient(circle closest-side at 20px 30px, red, green)"
+ );
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(closest-side ellipse at 20px 30px, red, green)",
+ "radial-gradient(closest-side at 20px 30px, red, green)"
+ );
+
+ // Parsing with <shape-keyword> and <size> reversed
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(closest-side circle at 20px 30px, red, green)",
+ "radial-gradient(circle closest-side at 20px 30px, red, green)"
+ );
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(closest-corner ellipse at 20px 30px, red, green)",
+ "radial-gradient(closest-corner at 20px 30px, red, green)"
+ );
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(30px circle, red, green)",
+ "radial-gradient(30px at center center, red, green)"
+ );
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(30px 40px ellipse, red, green)",
+ "radial-gradient(30px 40px at center center, red, green)"
+ );
+
+ // Parsing without <size>
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(circle, red, green)",
+ "radial-gradient(circle at center center, red, green)"
+ );
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(ellipse, red, green)",
+ "radial-gradient(at center center, red, green)"
+ );
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(circle at 20px 30px, red, green)"
+ );
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(ellipse at 20px 30px, red, green)",
+ "radial-gradient(at 20px 30px, red, green)"
+ );
+
+ // Parsing without <shape-keyword>
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(20px at 20px 30px, red, green)"
+ );
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(20px 30px at left center, red, green)"
+ );
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(closest-side at center, red, green)",
+ "radial-gradient(closest-side at center center, red, green)"
+ );
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(20px, red, green)",
+ "radial-gradient(20px at center center, red, green)"
+ );
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(20px 30px, red, green)",
+ "radial-gradient(20px 30px at center center, red, green)"
+ );
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(closest-side, red, green)",
+ "radial-gradient(closest-side at center center, red, green)"
+ );
+
+ // Parsing without <shape-keyword> and <size>
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(at center, red, green)",
+ "radial-gradient(at center center, red, green)"
+ );
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(at center bottom, red, green)"
+ );
+ assert_roundtrip_with_context!(Image::parse, "radial-gradient(at 40px 50px, red, green)");
+
+ // Parsing with just color stops
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "radial-gradient(red, green)",
+ "radial-gradient(at center center, red, green)"
+ );
+
+ // Parsing repeating radial gradient
+ assert_roundtrip_with_context!(
+ Image::parse,
+ "repeating-radial-gradient(red, green)",
+ "repeating-radial-gradient(at center center, red, green)"
+ );
+}
diff --git a/servo/tests/unit/style/parsing/inherited_text.rs b/servo/tests/unit/style/parsing/inherited_text.rs
new file mode 100644
index 0000000000..90ec16f7b0
--- /dev/null
+++ b/servo/tests/unit/style/parsing/inherited_text.rs
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use parsing::parse;
+use style::values::generics::text::Spacing;
+
+#[test]
+fn negative_letter_spacing_should_parse_properly() {
+ use style::properties::longhands::letter_spacing;
+ use style::values::specified::length::{FontRelativeLength, Length, NoCalcLength};
+
+ let negative_value = parse_longhand!(letter_spacing, "-0.5em");
+ let expected = Spacing::Value(Length::NoCalc(NoCalcLength::FontRelative(
+ FontRelativeLength::Em(-0.5),
+ )));
+ assert_eq!(negative_value, expected);
+}
+
+#[test]
+fn negative_word_spacing_should_parse_properly() {
+ use style::properties::longhands::word_spacing;
+ use style::values::specified::length::{FontRelativeLength, LengthPercentage, NoCalcLength};
+
+ let negative_value = parse_longhand!(word_spacing, "-0.5em");
+ let expected = Spacing::Value(LengthPercentage::Length(NoCalcLength::FontRelative(
+ FontRelativeLength::Em(-0.5),
+ )));
+ assert_eq!(negative_value, expected);
+}
+
+#[test]
+fn line_height_should_return_number_on_plain_zero() {
+ use style::properties::longhands::line_height;
+
+ let result = parse(line_height::parse, "0").unwrap();
+ assert_eq!(result, parse_longhand!(line_height, "0"));
+}
+
+#[test]
+fn line_height_should_return_length_on_length_zero() {
+ use style::properties::longhands::line_height;
+
+ let result = parse(line_height::parse, "0px").unwrap();
+ assert_eq!(result, parse_longhand!(line_height, "0px"));
+}
diff --git a/servo/tests/unit/style/parsing/mod.rs b/servo/tests/unit/style/parsing/mod.rs
new file mode 100644
index 0000000000..095f74c960
--- /dev/null
+++ b/servo/tests/unit/style/parsing/mod.rs
@@ -0,0 +1,150 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Tests for parsing and serialization of values/properties
+
+use cssparser::{Parser, ParserInput};
+use style::context::QuirksMode;
+use style::parser::ParserContext;
+use style::stylesheets::{CssRuleType, Origin};
+use style_traits::{ParseError, ParsingMode};
+
+fn parse<T, F>(f: F, s: &'static str) -> Result<T, ParseError<'static>>
+where
+ F: for<'t> Fn(&ParserContext, &mut Parser<'static, 't>) -> Result<T, ParseError<'static>>,
+{
+ let mut input = ParserInput::new(s);
+ parse_input(f, &mut input)
+}
+
+fn parse_input<'i: 't, 't, T, F>(f: F, input: &'t mut ParserInput<'i>) -> Result<T, ParseError<'i>>
+where
+ F: Fn(&ParserContext, &mut Parser<'i, 't>) -> Result<T, ParseError<'i>>,
+{
+ let url = ::servo_url::ServoUrl::parse("http://localhost").unwrap();
+ let context = ParserContext::new(
+ Origin::Author,
+ &url,
+ Some(CssRuleType::Style),
+ ParsingMode::DEFAULT,
+ QuirksMode::NoQuirks,
+ None,
+ None,
+ );
+ let mut parser = Parser::new(input);
+ f(&context, &mut parser)
+}
+
+fn parse_entirely<T, F>(f: F, s: &'static str) -> Result<T, ParseError<'static>>
+where
+ F: for<'t> Fn(&ParserContext, &mut Parser<'static, 't>) -> Result<T, ParseError<'static>>,
+{
+ let mut input = ParserInput::new(s);
+ parse_entirely_input(f, &mut input)
+}
+
+fn parse_entirely_input<'i: 't, 't, T, F>(
+ f: F,
+ input: &'t mut ParserInput<'i>,
+) -> Result<T, ParseError<'i>>
+where
+ F: Fn(&ParserContext, &mut Parser<'i, 't>) -> Result<T, ParseError<'i>>,
+{
+ parse_input(
+ |context, parser| parser.parse_entirely(|p| f(context, p)),
+ input,
+ )
+}
+
+// This is a macro so that the file/line information
+// is preserved in the panic
+macro_rules! assert_roundtrip_with_context {
+ ($fun:expr, $string:expr) => {
+ assert_roundtrip_with_context!($fun, $string, $string);
+ };
+ ($fun:expr, $input:expr, $output:expr) => {{
+ let mut input = ::cssparser::ParserInput::new($input);
+ let serialized = super::parse_input(
+ |context, i| {
+ let parsed = $fun(context, i).expect(&format!("Failed to parse {}", $input));
+ let serialized = ToCss::to_css_string(&parsed);
+ assert_eq!(serialized, $output);
+ Ok(serialized)
+ },
+ &mut input,
+ )
+ .unwrap();
+
+ let mut input = ::cssparser::ParserInput::new(&serialized);
+ let unwrapped = super::parse_input(
+ |context, i| {
+ let re_parsed =
+ $fun(context, i).expect(&format!("Failed to parse serialization {}", $input));
+ let re_serialized = ToCss::to_css_string(&re_parsed);
+ assert_eq!(serialized, re_serialized);
+ Ok(())
+ },
+ &mut input,
+ )
+ .unwrap();
+ unwrapped
+ }};
+}
+
+macro_rules! assert_roundtrip {
+ ($fun:expr, $string:expr) => {
+ assert_roundtrip!($fun, $string, $string);
+ };
+ ($fun:expr, $input:expr, $output:expr) => {
+ let mut input = ParserInput::new($input);
+ let mut parser = Parser::new(&mut input);
+ let parsed = $fun(&mut parser).expect(&format!("Failed to parse {}", $input));
+ let serialized = ToCss::to_css_string(&parsed);
+ assert_eq!(serialized, $output);
+
+ let mut input = ParserInput::new(&serialized);
+ let mut parser = Parser::new(&mut input);
+ let re_parsed =
+ $fun(&mut parser).expect(&format!("Failed to parse serialization {}", $input));
+ let re_serialized = ToCss::to_css_string(&re_parsed);
+ assert_eq!(serialized, re_serialized)
+ };
+}
+
+macro_rules! assert_parser_exhausted {
+ ($fun:expr, $string:expr, $should_exhausted:expr) => {{
+ parse(
+ |context, input| {
+ let parsed = $fun(context, input);
+ assert_eq!(parsed.is_ok(), true);
+ assert_eq!(input.is_exhausted(), $should_exhausted);
+ Ok(())
+ },
+ $string,
+ )
+ .unwrap()
+ }};
+}
+
+macro_rules! parse_longhand {
+ ($name:ident, $s:expr) => {
+ parse($name::parse, $s).unwrap()
+ };
+}
+
+mod animation;
+mod background;
+mod border;
+mod box_;
+mod column;
+mod effects;
+mod image;
+mod inherited_text;
+mod outline;
+mod position;
+mod selectors;
+mod supports;
+mod text_overflow;
+mod transition_duration;
+mod transition_timing_function;
diff --git a/servo/tests/unit/style/parsing/outline.rs b/servo/tests/unit/style/parsing/outline.rs
new file mode 100644
index 0000000000..6d909d9bc5
--- /dev/null
+++ b/servo/tests/unit/style/parsing/outline.rs
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use parsing::parse;
+use style_traits::ToCss;
+
+#[test]
+fn test_outline_style() {
+ use style::properties::longhands::outline_style;
+
+ assert_roundtrip_with_context!(outline_style::parse, r#"auto"#);
+ assert_roundtrip_with_context!(outline_style::parse, r#"none"#);
+ assert_roundtrip_with_context!(outline_style::parse, r#"solid"#);
+ assert_roundtrip_with_context!(outline_style::parse, r#"double"#);
+ assert_roundtrip_with_context!(outline_style::parse, r#"dotted"#);
+ assert_roundtrip_with_context!(outline_style::parse, r#"dashed"#);
+ assert_roundtrip_with_context!(outline_style::parse, r#"groove"#);
+ assert_roundtrip_with_context!(outline_style::parse, r#"ridge"#);
+ assert_roundtrip_with_context!(outline_style::parse, r#"inset"#);
+ assert_roundtrip_with_context!(outline_style::parse, r#"outset"#);
+
+ // The outline-style property accepts the same values as border-style,
+ // except that 'hidden' is not a legal outline style.
+ assert!(parse(outline_style::parse, r#"hidden"#).is_err());
+}
diff --git a/servo/tests/unit/style/parsing/position.rs b/servo/tests/unit/style/parsing/position.rs
new file mode 100644
index 0000000000..ce73c7a0d2
--- /dev/null
+++ b/servo/tests/unit/style/parsing/position.rs
@@ -0,0 +1,145 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use parsing::{parse, parse_entirely};
+use style::parser::Parse;
+use style::values::specified::position::*;
+use style_traits::ToCss;
+
+#[test]
+fn test_position() {
+ // Serialization is not actually specced
+ // though these are the values expected by basic-shape
+ // https://github.com/w3c/csswg-drafts/issues/368
+ assert_roundtrip_with_context!(Position::parse, "center", "center center");
+ assert_roundtrip_with_context!(Position::parse, "top left", "left top");
+ assert_roundtrip_with_context!(Position::parse, "left top", "left top");
+ assert_roundtrip_with_context!(Position::parse, "top right", "right top");
+ assert_roundtrip_with_context!(Position::parse, "right top", "right top");
+ assert_roundtrip_with_context!(Position::parse, "bottom left", "left bottom");
+ assert_roundtrip_with_context!(Position::parse, "left bottom", "left bottom");
+ assert_roundtrip_with_context!(Position::parse, "left center", "left center");
+ assert_roundtrip_with_context!(Position::parse, "right center", "right center");
+ assert_roundtrip_with_context!(Position::parse, "center top", "center top");
+ assert_roundtrip_with_context!(Position::parse, "center bottom", "center bottom");
+ assert_roundtrip_with_context!(Position::parse, "center 10px", "center 10px");
+ assert_roundtrip_with_context!(Position::parse, "center 10%", "center 10%");
+ assert_roundtrip_with_context!(Position::parse, "right 10%", "right 10%");
+
+ // Only keywords can be reordered
+ assert!(parse_entirely(Position::parse, "top 40%").is_err());
+ assert!(parse_entirely(Position::parse, "40% left").is_err());
+
+ // 3 and 4 value serialization
+ assert_roundtrip_with_context!(Position::parse, "left 10px top 15px", "left 10px top 15px");
+ assert_roundtrip_with_context!(Position::parse, "top 15px left 10px", "left 10px top 15px");
+ assert_roundtrip_with_context!(Position::parse, "left 10% top 15px", "left 10% top 15px");
+ assert_roundtrip_with_context!(Position::parse, "top 15px left 10%", "left 10% top 15px");
+ assert_roundtrip_with_context!(Position::parse, "left top 15px", "left top 15px");
+ assert_roundtrip_with_context!(Position::parse, "top 15px left", "left top 15px");
+ assert_roundtrip_with_context!(Position::parse, "left 10px top", "left 10px top");
+ assert_roundtrip_with_context!(Position::parse, "top left 10px", "left 10px top");
+ assert_roundtrip_with_context!(Position::parse, "right 10px bottom", "right 10px bottom");
+ assert_roundtrip_with_context!(Position::parse, "bottom right 10px", "right 10px bottom");
+ assert_roundtrip_with_context!(Position::parse, "center right 10px", "right 10px center");
+ assert_roundtrip_with_context!(Position::parse, "center bottom 10px", "center bottom 10px");
+
+ // Invalid 3 value positions
+ assert!(parse_entirely(Position::parse, "20px 30px 20px").is_err());
+ assert!(parse_entirely(Position::parse, "top 30px 20px").is_err());
+ assert!(parse_entirely(Position::parse, "50% bottom 20%").is_err());
+
+ // Only horizontal and vertical keywords can have positions
+ assert!(parse_entirely(Position::parse, "center 10px left 15px").is_err());
+ assert!(parse_entirely(Position::parse, "center 10px 15px").is_err());
+ assert!(parse_entirely(Position::parse, "center 10px bottom").is_err());
+
+ // "Horizontal Horizontal" or "Vertical Vertical" positions cause error
+ assert!(parse_entirely(Position::parse, "left right").is_err());
+ assert!(parse_entirely(Position::parse, "left 10px right").is_err());
+ assert!(parse_entirely(Position::parse, "left 10px right 15%").is_err());
+ assert!(parse_entirely(Position::parse, "top bottom").is_err());
+ assert!(parse_entirely(Position::parse, "top 10px bottom").is_err());
+ assert!(parse_entirely(Position::parse, "top 10px bottom 15%").is_err());
+
+ // Logical keywords are not supported in Position yet.
+ assert!(parse(Position::parse, "x-start").is_err());
+ assert!(parse(Position::parse, "y-end").is_err());
+ assert!(parse(Position::parse, "x-start y-end").is_err());
+ assert!(parse(Position::parse, "x-end 10px").is_err());
+ assert!(parse(Position::parse, "y-start 20px").is_err());
+ assert!(parse(Position::parse, "x-start bottom 10%").is_err());
+ assert!(parse_entirely(Position::parse, "left y-start 10%").is_err());
+ assert!(parse(Position::parse, "x-start 20px y-end 10%").is_err());
+}
+
+#[test]
+fn test_horizontal_position() {
+ // One value serializations.
+ assert_roundtrip_with_context!(HorizontalPosition::parse, "20px", "20px");
+ assert_roundtrip_with_context!(HorizontalPosition::parse, "25%", "25%");
+ assert_roundtrip_with_context!(HorizontalPosition::parse, "center", "center");
+ assert_roundtrip_with_context!(HorizontalPosition::parse, "left", "left");
+ assert_roundtrip_with_context!(HorizontalPosition::parse, "right", "right");
+
+ // Two value serializations.
+ assert_roundtrip_with_context!(HorizontalPosition::parse, "right 10px", "right 10px");
+
+ // Invalid horizontal positions.
+ assert!(parse(HorizontalPosition::parse, "top").is_err());
+ assert!(parse(HorizontalPosition::parse, "bottom").is_err());
+ assert!(parse(HorizontalPosition::parse, "y-start").is_err());
+ assert!(parse(HorizontalPosition::parse, "y-end").is_err());
+ assert!(parse(HorizontalPosition::parse, "y-end 20px ").is_err());
+ assert!(parse(HorizontalPosition::parse, "bottom 20px").is_err());
+ assert!(parse(HorizontalPosition::parse, "bottom top").is_err());
+ assert!(parse_entirely(HorizontalPosition::parse, "20px y-end").is_err());
+ assert!(parse_entirely(HorizontalPosition::parse, "20px top").is_err());
+ assert!(parse_entirely(HorizontalPosition::parse, "left center").is_err());
+ assert!(parse_entirely(HorizontalPosition::parse, "left top").is_err());
+ assert!(parse_entirely(HorizontalPosition::parse, "left right").is_err());
+ assert!(parse_entirely(HorizontalPosition::parse, "20px 30px").is_err());
+ assert!(parse_entirely(HorizontalPosition::parse, "10px left").is_err());
+ assert!(parse_entirely(HorizontalPosition::parse, "x-end 20%").is_err());
+ assert!(parse_entirely(HorizontalPosition::parse, "20px x-start").is_err());
+
+ // Logical keywords are not supported in Position yet.
+ assert!(parse(HorizontalPosition::parse, "x-start").is_err());
+ assert!(parse(HorizontalPosition::parse, "x-end").is_err());
+}
+
+#[test]
+fn test_vertical_position() {
+ // One value serializations.
+ assert_roundtrip_with_context!(VerticalPosition::parse, "20px", "20px");
+ assert_roundtrip_with_context!(VerticalPosition::parse, "25%", "25%");
+ assert_roundtrip_with_context!(VerticalPosition::parse, "center", "center");
+ assert_roundtrip_with_context!(VerticalPosition::parse, "top", "top");
+ assert_roundtrip_with_context!(VerticalPosition::parse, "bottom", "bottom");
+
+ // Two value serializations.
+ assert_roundtrip_with_context!(VerticalPosition::parse, "bottom 10px", "bottom 10px");
+
+ // Invalid vertical positions.
+ assert!(parse(VerticalPosition::parse, "left").is_err());
+ assert!(parse(VerticalPosition::parse, "right").is_err());
+ assert!(parse(VerticalPosition::parse, "x-start").is_err());
+ assert!(parse(VerticalPosition::parse, "x-end").is_err());
+ assert!(parse(VerticalPosition::parse, "x-end 20px").is_err());
+ assert!(parse(VerticalPosition::parse, "left 20px").is_err());
+ assert!(parse(VerticalPosition::parse, "left center").is_err());
+ assert!(parse(VerticalPosition::parse, "left top").is_err());
+ assert!(parse(VerticalPosition::parse, "left right").is_err());
+ assert!(parse_entirely(VerticalPosition::parse, "20px x-end").is_err());
+ assert!(parse_entirely(VerticalPosition::parse, "20px right").is_err());
+ assert!(parse_entirely(VerticalPosition::parse, "bottom top").is_err());
+ assert!(parse_entirely(VerticalPosition::parse, "20px 30px").is_err());
+ assert!(parse_entirely(VerticalPosition::parse, "10px top").is_err());
+ assert!(parse_entirely(VerticalPosition::parse, "y-end 20%").is_err());
+ assert!(parse_entirely(VerticalPosition::parse, "20px y-start").is_err());
+
+ // Logical keywords are not supported in Position yet.
+ assert!(parse(VerticalPosition::parse, "y-start").is_err());
+ assert!(parse(VerticalPosition::parse, "y-end").is_err());
+}
diff --git a/servo/tests/unit/style/parsing/selectors.rs b/servo/tests/unit/style/parsing/selectors.rs
new file mode 100644
index 0000000000..1ce7498caa
--- /dev/null
+++ b/servo/tests/unit/style/parsing/selectors.rs
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use cssparser::{Parser, ParserInput, ToCss};
+use selectors::parser::SelectorList;
+use style::selector_parser::{SelectorImpl, SelectorParser};
+use style::stylesheets::{Namespaces, Origin};
+use style_traits::ParseError;
+
+fn parse_selector<'i, 't>(
+ input: &mut Parser<'i, 't>,
+) -> Result<SelectorList<SelectorImpl>, ParseError<'i>> {
+ let mut ns = Namespaces::default();
+ ns.prefixes.insert("svg".into(), ns!(svg));
+ let parser = SelectorParser {
+ stylesheet_origin: Origin::UserAgent,
+ namespaces: &ns,
+ url_data: None,
+ };
+ SelectorList::parse(&parser, input)
+}
+
+#[test]
+fn test_selectors() {
+ assert_roundtrip!(parse_selector, "div");
+ assert_roundtrip!(parse_selector, "svg|circle");
+ assert_roundtrip!(parse_selector, "p:before", "p::before");
+ assert_roundtrip!(
+ parse_selector,
+ "[border=\"0\"]:-servo-nonzero-border ~ ::-servo-details-summary"
+ );
+ assert_roundtrip!(parse_selector, "* > *");
+ assert_roundtrip!(parse_selector, "*|* + *", "* + *");
+}
diff --git a/servo/tests/unit/style/parsing/supports.rs b/servo/tests/unit/style/parsing/supports.rs
new file mode 100644
index 0000000000..f54ee48bab
--- /dev/null
+++ b/servo/tests/unit/style/parsing/supports.rs
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use cssparser::{Parser, ParserInput};
+use style::stylesheets::supports_rule::SupportsCondition;
+use style_traits::ToCss;
+
+#[test]
+fn test_supports_condition() {
+ assert_roundtrip!(SupportsCondition::parse, "(margin: 1px)");
+ assert_roundtrip!(SupportsCondition::parse, "not (--be: to be)");
+ assert_roundtrip!(
+ SupportsCondition::parse,
+ "(color: blue) and future-extension(4)"
+ );
+ assert_roundtrip!(SupportsCondition::parse, "future-\\1 extension(4)");
+ assert_roundtrip!(SupportsCondition::parse, "((test))");
+}
diff --git a/servo/tests/unit/style/parsing/text_overflow.rs b/servo/tests/unit/style/parsing/text_overflow.rs
new file mode 100644
index 0000000000..505e6ffb8c
--- /dev/null
+++ b/servo/tests/unit/style/parsing/text_overflow.rs
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use parsing::parse;
+use style_traits::ToCss;
+
+#[test]
+fn test_text_overflow() {
+ use style::properties::longhands::text_overflow;
+
+ assert_roundtrip_with_context!(text_overflow::parse, r#"clip"#);
+ assert_roundtrip_with_context!(text_overflow::parse, r#"ellipsis"#);
+ assert_roundtrip_with_context!(text_overflow::parse, r#"clip ellipsis"#);
+ assert_roundtrip_with_context!(text_overflow::parse, r#""x""#);
+ assert_roundtrip_with_context!(text_overflow::parse, r#"'x'"#, r#""x""#);
+ assert_roundtrip_with_context!(text_overflow::parse, r#"clip "x""#);
+ assert_roundtrip_with_context!(text_overflow::parse, r#""x" clip"#);
+ assert_roundtrip_with_context!(text_overflow::parse, r#""x" "y""#);
+}
+
+#[test]
+fn test_text_overflow_parser_exhaustion() {
+ use style::properties::longhands::text_overflow;
+
+ assert_parser_exhausted!(text_overflow::parse, r#"clip rubbish"#, false);
+ assert_parser_exhausted!(text_overflow::parse, r#"clip"#, true);
+ assert_parser_exhausted!(text_overflow::parse, r#"ellipsis"#, true);
+ assert_parser_exhausted!(text_overflow::parse, r#"clip ellipsis"#, true);
+}
diff --git a/servo/tests/unit/style/parsing/transition_duration.rs b/servo/tests/unit/style/parsing/transition_duration.rs
new file mode 100644
index 0000000000..c771a58944
--- /dev/null
+++ b/servo/tests/unit/style/parsing/transition_duration.rs
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use parsing::parse;
+use style::properties::longhands::transition_duration;
+
+#[test]
+fn test_positive_transition_duration() {
+ assert!(parse(transition_duration::parse, "5s").is_ok());
+ assert!(parse(transition_duration::parse, "0s").is_ok());
+}
+
+#[test]
+fn test_negative_transition_duration() {
+ assert!(parse(transition_duration::parse, "-5s").is_err());
+}
diff --git a/servo/tests/unit/style/parsing/transition_timing_function.rs b/servo/tests/unit/style/parsing/transition_timing_function.rs
new file mode 100644
index 0000000000..5fbb89709e
--- /dev/null
+++ b/servo/tests/unit/style/parsing/transition_timing_function.rs
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use parsing::parse;
+use style::properties::longhands::transition_timing_function;
+use style_traits::ToCss;
+
+#[test]
+fn test_cubic_bezier() {
+ assert_roundtrip_with_context!(
+ transition_timing_function::parse,
+ "cubic-bezier(0, 0, 0, 0)"
+ );
+ assert_roundtrip_with_context!(
+ transition_timing_function::parse,
+ "cubic-bezier(0.25, 0, 0.5, 0)"
+ );
+ assert_roundtrip_with_context!(
+ transition_timing_function::parse,
+ "cubic-bezier(1, 1, 1, 1)"
+ );
+
+ // p1x and p2x values must be in range [0, 1]
+ assert!(parse(
+ transition_timing_function::parse,
+ "cubic-bezier(-1, 0, 0, 0"
+ )
+ .is_err());
+ assert!(parse(
+ transition_timing_function::parse,
+ "cubic-bezier(0, 0, -1, 0"
+ )
+ .is_err());
+ assert!(parse(
+ transition_timing_function::parse,
+ "cubic-bezier(-1, 0, -1, 0"
+ )
+ .is_err());
+
+ assert!(parse(transition_timing_function::parse, "cubic-bezier(2, 0, 0, 0").is_err());
+ assert!(parse(transition_timing_function::parse, "cubic-bezier(0, 0, 2, 0").is_err());
+ assert!(parse(transition_timing_function::parse, "cubic-bezier(2, 0, 2, 0").is_err());
+}
+
+#[test]
+fn test_steps() {
+ assert_roundtrip_with_context!(transition_timing_function::parse, "steps(1)");
+ assert_roundtrip_with_context!(transition_timing_function::parse, "steps( 1)", "steps(1)");
+ assert_roundtrip_with_context!(transition_timing_function::parse, "steps(1, start)");
+ assert_roundtrip_with_context!(
+ transition_timing_function::parse,
+ "steps(2, end) ",
+ "steps(2)"
+ );
+
+ // Step interval value must be an integer greater than 0
+ assert!(parse(transition_timing_function::parse, "steps(0)").is_err());
+ assert!(parse(transition_timing_function::parse, "steps(0.5)").is_err());
+ assert!(parse(transition_timing_function::parse, "steps(-1)").is_err());
+ assert!(parse(transition_timing_function::parse, "steps(1, middle)").is_err());
+}
diff --git a/servo/tests/unit/style/properties/mod.rs b/servo/tests/unit/style/properties/mod.rs
new file mode 100644
index 0000000000..06a5c2086f
--- /dev/null
+++ b/servo/tests/unit/style/properties/mod.rs
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use cssparser::{Parser, ParserInput};
+use style::context::QuirksMode;
+use style::parser::ParserContext;
+use style::stylesheets::{CssRuleType, Origin};
+use style_traits::{ParseError, ParsingMode};
+
+fn parse<T, F>(f: F, s: &'static str) -> Result<T, ParseError<'static>>
+where
+ F: for<'t> Fn(&ParserContext, &mut Parser<'static, 't>) -> Result<T, ParseError<'static>>,
+{
+ let mut input = ParserInput::new(s);
+ parse_input(f, &mut input)
+}
+
+fn parse_input<'i: 't, 't, T, F>(f: F, input: &'t mut ParserInput<'i>) -> Result<T, ParseError<'i>>
+where
+ F: Fn(&ParserContext, &mut Parser<'i, 't>) -> Result<T, ParseError<'i>>,
+{
+ let url = ::servo_url::ServoUrl::parse("http://localhost").unwrap();
+ let context = ParserContext::new(
+ Origin::Author,
+ &url,
+ Some(CssRuleType::Style),
+ ParsingMode::DEFAULT,
+ QuirksMode::NoQuirks,
+ None,
+ None,
+ );
+ let mut parser = Parser::new(input);
+ f(&context, &mut parser)
+}
+
+macro_rules! assert_roundtrip_with_context {
+ ($fun:expr, $string:expr) => {
+ assert_roundtrip_with_context!($fun, $string, $string);
+ };
+ ($fun:expr, $input:expr, $output:expr) => {{
+ let serialized = parse(
+ |context, i| {
+ let parsed = $fun(context, i).expect(&format!("Failed to parse {}", $input));
+ let serialized = ToCss::to_css_string(&parsed);
+ assert_eq!(serialized, $output);
+ Ok(serialized)
+ },
+ $input,
+ )
+ .unwrap();
+
+ let mut input = ::cssparser::ParserInput::new(&serialized);
+ let unwrapped = parse_input(
+ |context, i| {
+ let re_parsed =
+ $fun(context, i).expect(&format!("Failed to parse serialization {}", $input));
+ let re_serialized = ToCss::to_css_string(&re_parsed);
+ assert_eq!(serialized, re_serialized);
+ Ok(())
+ },
+ &mut input,
+ )
+ .unwrap();
+ unwrapped
+ }};
+}
+
+mod scaffolding;
+mod serialization;
diff --git a/servo/tests/unit/style/properties/scaffolding.rs b/servo/tests/unit/style/properties/scaffolding.rs
new file mode 100644
index 0000000000..68e1b96ddf
--- /dev/null
+++ b/servo/tests/unit/style/properties/scaffolding.rs
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use serde_json::{self, Value};
+use std::env;
+use std::fs::{remove_file, File};
+use std::path::Path;
+use std::process::Command;
+
+#[test]
+fn properties_list_json() {
+ let top = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap())
+ .join("..")
+ .join("..")
+ .join("..");
+ let json = top
+ .join("target")
+ .join("doc")
+ .join("servo")
+ .join("css-properties.json");
+ if json.exists() {
+ remove_file(&json).unwrap()
+ }
+ let python = env::var("PYTHON").ok().unwrap_or_else(find_python);
+ let script = top
+ .join("components")
+ .join("style")
+ .join("properties")
+ .join("build.py");
+ let status = Command::new(python)
+ .arg(&script)
+ .arg("servo")
+ .arg("html")
+ .arg("regular")
+ .status()
+ .unwrap();
+ assert!(status.success(), "{:?}", status);
+
+ let properties: Value = serde_json::from_reader(File::open(json).unwrap()).unwrap();
+ assert!(properties.as_object().unwrap().len() > 100);
+ assert!(properties.as_object().unwrap().contains_key("margin"));
+ assert!(properties.as_object().unwrap().contains_key("margin-top"));
+}
+
+#[cfg(windows)]
+fn find_python() -> String {
+ if Command::new("python2.7.exe")
+ .arg("--version")
+ .output()
+ .is_ok()
+ {
+ return "python2.7.exe".to_owned();
+ }
+
+ if Command::new("python27.exe")
+ .arg("--version")
+ .output()
+ .is_ok()
+ {
+ return "python27.exe".to_owned();
+ }
+
+ if Command::new("python.exe").arg("--version").output().is_ok() {
+ return "python.exe".to_owned();
+ }
+
+ panic!("Can't find python (tried python27.exe and python.exe)! Try fixing PATH or setting the PYTHON env var");
+}
+
+#[cfg(not(windows))]
+fn find_python() -> String {
+ if Command::new("python2.7")
+ .arg("--version")
+ .output()
+ .unwrap()
+ .status
+ .success()
+ {
+ "python2.7"
+ } else {
+ "python"
+ }
+ .to_owned()
+}
diff --git a/servo/tests/unit/style/properties/serialization.rs b/servo/tests/unit/style/properties/serialization.rs
new file mode 100644
index 0000000000..86fb09039e
--- /dev/null
+++ b/servo/tests/unit/style/properties/serialization.rs
@@ -0,0 +1,773 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use properties::{parse, parse_input};
+use style::computed_values::display::T as Display;
+use style::properties::declaration_block::PropertyDeclarationBlock;
+use style::properties::parse_property_declaration_list;
+use style::properties::{Importance, PropertyDeclaration};
+use style::values::specified::url::SpecifiedUrl;
+use style::values::specified::NoCalcLength;
+use style::values::specified::{BorderSideWidth, BorderStyle, Color};
+use style::values::specified::{Length, LengthPercentage, LengthPercentageOrAuto};
+use style::values::RGBA;
+use style_traits::ToCss;
+use stylesheets::block_from;
+
+trait ToCssString {
+ fn to_css_string(&self) -> String;
+}
+
+impl ToCssString for PropertyDeclarationBlock {
+ fn to_css_string(&self) -> String {
+ let mut css = String::new();
+ self.to_css(&mut css).unwrap();
+ css
+ }
+}
+
+#[test]
+fn property_declaration_block_should_serialize_correctly() {
+ use style::properties::longhands::overflow_x::SpecifiedValue as OverflowValue;
+
+ let declarations = vec![
+ (
+ PropertyDeclaration::Width(LengthPercentageOrAuto::Length(NoCalcLength::from_px(
+ 70f32,
+ ))),
+ Importance::Normal,
+ ),
+ (
+ PropertyDeclaration::MinHeight(LengthPercentage::Length(NoCalcLength::from_px(20f32))),
+ Importance::Normal,
+ ),
+ (
+ PropertyDeclaration::Height(LengthPercentageOrAuto::Length(NoCalcLength::from_px(
+ 20f32,
+ ))),
+ Importance::Important,
+ ),
+ (
+ PropertyDeclaration::Display(Display::InlineBlock),
+ Importance::Normal,
+ ),
+ (
+ PropertyDeclaration::OverflowX(OverflowValue::Auto),
+ Importance::Normal,
+ ),
+ (
+ PropertyDeclaration::OverflowY(OverflowValue::Auto),
+ Importance::Normal,
+ ),
+ ];
+
+ let block = block_from(declarations);
+
+ let css_string = block.to_css_string();
+
+ assert_eq!(
+ css_string,
+ "width: 70px; min-height: 20px; height: 20px !important; display: inline-block; overflow: auto;"
+ );
+}
+
+mod shorthand_serialization {
+ pub use super::*;
+
+ pub fn shorthand_properties_to_string(properties: Vec<PropertyDeclaration>) -> String {
+ let block = block_from(properties.into_iter().map(|d| (d, Importance::Normal)));
+
+ block.to_css_string()
+ }
+
+ mod four_sides_shorthands {
+ pub use super::*;
+
+ // we can use margin as a base to test out the different combinations
+ // but afterwards, we only need to to one test per "four sides shorthand"
+ #[test]
+ fn all_equal_properties_should_serialize_to_one_value() {
+ let mut properties = Vec::new();
+
+ let px_70 = LengthPercentageOrAuto::Length(NoCalcLength::from_px(70f32));
+ properties.push(PropertyDeclaration::MarginTop(px_70.clone()));
+ properties.push(PropertyDeclaration::MarginRight(px_70.clone()));
+ properties.push(PropertyDeclaration::MarginBottom(px_70.clone()));
+ properties.push(PropertyDeclaration::MarginLeft(px_70));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(serialization, "margin: 70px;");
+ }
+
+ #[test]
+ fn equal_vertical_and_equal_horizontal_properties_should_serialize_to_two_value() {
+ let mut properties = Vec::new();
+
+ let vertical_px = LengthPercentageOrAuto::Length(NoCalcLength::from_px(10f32));
+ let horizontal_px = LengthPercentageOrAuto::Length(NoCalcLength::from_px(5f32));
+
+ properties.push(PropertyDeclaration::MarginTop(vertical_px.clone()));
+ properties.push(PropertyDeclaration::MarginRight(horizontal_px.clone()));
+ properties.push(PropertyDeclaration::MarginBottom(vertical_px));
+ properties.push(PropertyDeclaration::MarginLeft(horizontal_px));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(serialization, "margin: 10px 5px;");
+ }
+
+ #[test]
+ fn different_vertical_and_equal_horizontal_properties_should_serialize_to_three_values() {
+ let mut properties = Vec::new();
+
+ let top_px = LengthPercentageOrAuto::Length(NoCalcLength::from_px(8f32));
+ let bottom_px = LengthPercentageOrAuto::Length(NoCalcLength::from_px(10f32));
+ let horizontal_px = LengthPercentageOrAuto::Length(NoCalcLength::from_px(5f32));
+
+ properties.push(PropertyDeclaration::MarginTop(top_px));
+ properties.push(PropertyDeclaration::MarginRight(horizontal_px.clone()));
+ properties.push(PropertyDeclaration::MarginBottom(bottom_px));
+ properties.push(PropertyDeclaration::MarginLeft(horizontal_px));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(serialization, "margin: 8px 5px 10px;");
+ }
+
+ #[test]
+ fn different_properties_should_serialize_to_four_values() {
+ let mut properties = Vec::new();
+
+ let top_px = LengthPercentageOrAuto::Length(NoCalcLength::from_px(8f32));
+ let right_px = LengthPercentageOrAuto::Length(NoCalcLength::from_px(12f32));
+ let bottom_px = LengthPercentageOrAuto::Length(NoCalcLength::from_px(10f32));
+ let left_px = LengthPercentageOrAuto::Length(NoCalcLength::from_px(14f32));
+
+ properties.push(PropertyDeclaration::MarginTop(top_px));
+ properties.push(PropertyDeclaration::MarginRight(right_px));
+ properties.push(PropertyDeclaration::MarginBottom(bottom_px));
+ properties.push(PropertyDeclaration::MarginLeft(left_px));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(serialization, "margin: 8px 12px 10px 14px;");
+ }
+
+ #[test]
+ fn different_longhands_should_serialize_to_long_form() {
+ let mut properties = Vec::new();
+
+ let solid = BorderStyle::Solid;
+
+ properties.push(PropertyDeclaration::BorderTopStyle(solid.clone()));
+ properties.push(PropertyDeclaration::BorderRightStyle(solid.clone()));
+ properties.push(PropertyDeclaration::BorderBottomStyle(solid.clone()));
+ properties.push(PropertyDeclaration::BorderLeftStyle(solid.clone()));
+
+ let px_30 = BorderSideWidth::Length(Length::from_px(30f32));
+ let px_10 = BorderSideWidth::Length(Length::from_px(10f32));
+
+ properties.push(PropertyDeclaration::BorderTopWidth(px_30.clone()));
+ properties.push(PropertyDeclaration::BorderRightWidth(px_30.clone()));
+ properties.push(PropertyDeclaration::BorderBottomWidth(px_30.clone()));
+ properties.push(PropertyDeclaration::BorderLeftWidth(px_10.clone()));
+
+ let blue = Color::rgba(RGBA::new(0, 0, 255, 255));
+
+ properties.push(PropertyDeclaration::BorderTopColor(blue.clone()));
+ properties.push(PropertyDeclaration::BorderRightColor(blue.clone()));
+ properties.push(PropertyDeclaration::BorderBottomColor(blue.clone()));
+ properties.push(PropertyDeclaration::BorderLeftColor(blue.clone()));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(serialization,
+ "border-style: solid; border-width: 30px 30px 30px 10px; border-color: rgb(0, 0, 255);");
+ }
+
+ #[test]
+ fn same_longhands_should_serialize_correctly() {
+ let mut properties = Vec::new();
+
+ let solid = BorderStyle::Solid;
+
+ properties.push(PropertyDeclaration::BorderTopStyle(solid.clone()));
+ properties.push(PropertyDeclaration::BorderRightStyle(solid.clone()));
+ properties.push(PropertyDeclaration::BorderBottomStyle(solid.clone()));
+ properties.push(PropertyDeclaration::BorderLeftStyle(solid.clone()));
+
+ let px_30 = BorderSideWidth::Length(Length::from_px(30f32));
+
+ properties.push(PropertyDeclaration::BorderTopWidth(px_30.clone()));
+ properties.push(PropertyDeclaration::BorderRightWidth(px_30.clone()));
+ properties.push(PropertyDeclaration::BorderBottomWidth(px_30.clone()));
+ properties.push(PropertyDeclaration::BorderLeftWidth(px_30.clone()));
+
+ let blue = Color::rgba(RGBA::new(0, 0, 255, 255));
+
+ properties.push(PropertyDeclaration::BorderTopColor(blue.clone()));
+ properties.push(PropertyDeclaration::BorderRightColor(blue.clone()));
+ properties.push(PropertyDeclaration::BorderBottomColor(blue.clone()));
+ properties.push(PropertyDeclaration::BorderLeftColor(blue.clone()));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(
+ serialization,
+ "border-style: solid; border-width: 30px; border-color: rgb(0, 0, 255);"
+ );
+ }
+
+ #[test]
+ fn padding_should_serialize_correctly() {
+ use style::values::specified::NonNegativeLengthPercentage;
+
+ let mut properties = Vec::new();
+
+ let px_10: NonNegativeLengthPercentage = NoCalcLength::from_px(10f32).into();
+ let px_15: NonNegativeLengthPercentage = NoCalcLength::from_px(15f32).into();
+ properties.push(PropertyDeclaration::PaddingTop(px_10.clone()));
+ properties.push(PropertyDeclaration::PaddingRight(px_15.clone()));
+ properties.push(PropertyDeclaration::PaddingBottom(px_10));
+ properties.push(PropertyDeclaration::PaddingLeft(px_15));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(serialization, "padding: 10px 15px;");
+ }
+
+ #[test]
+ fn border_width_should_serialize_correctly() {
+ let mut properties = Vec::new();
+
+ let top_px = BorderSideWidth::Length(Length::from_px(10f32));
+ let bottom_px = BorderSideWidth::Length(Length::from_px(10f32));
+
+ let right_px = BorderSideWidth::Length(Length::from_px(15f32));
+ let left_px = BorderSideWidth::Length(Length::from_px(15f32));
+
+ properties.push(PropertyDeclaration::BorderTopWidth(top_px));
+ properties.push(PropertyDeclaration::BorderRightWidth(right_px));
+ properties.push(PropertyDeclaration::BorderBottomWidth(bottom_px));
+ properties.push(PropertyDeclaration::BorderLeftWidth(left_px));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(serialization, "border-width: 10px 15px;");
+ }
+
+ #[test]
+ fn border_width_with_keywords_should_serialize_correctly() {
+ let mut properties = Vec::new();
+
+ let top_px = BorderSideWidth::Thin;
+ let right_px = BorderSideWidth::Medium;
+ let bottom_px = BorderSideWidth::Thick;
+ let left_px = BorderSideWidth::Length(Length::from_px(15f32));
+
+ properties.push(PropertyDeclaration::BorderTopWidth(top_px));
+ properties.push(PropertyDeclaration::BorderRightWidth(right_px));
+ properties.push(PropertyDeclaration::BorderBottomWidth(bottom_px));
+ properties.push(PropertyDeclaration::BorderLeftWidth(left_px));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(serialization, "border-width: thin medium thick 15px;");
+ }
+
+ #[test]
+ fn border_color_should_serialize_correctly() {
+ let mut properties = Vec::new();
+
+ let red = Color::rgba(RGBA::new(255, 0, 0, 255));
+ let blue = Color::rgba(RGBA::new(0, 0, 255, 255));
+
+ properties.push(PropertyDeclaration::BorderTopColor(blue.clone()));
+ properties.push(PropertyDeclaration::BorderRightColor(red.clone()));
+ properties.push(PropertyDeclaration::BorderBottomColor(blue));
+ properties.push(PropertyDeclaration::BorderLeftColor(red));
+
+ let serialization = shorthand_properties_to_string(properties);
+
+ // TODO: Make the rgb test show border-color as blue red instead of below tuples
+ assert_eq!(
+ serialization,
+ "border-color: rgb(0, 0, 255) rgb(255, 0, 0);"
+ );
+ }
+
+ #[test]
+ fn border_style_should_serialize_correctly() {
+ let mut properties = Vec::new();
+
+ let solid = BorderStyle::Solid;
+ let dotted = BorderStyle::Dotted;
+ properties.push(PropertyDeclaration::BorderTopStyle(solid.clone()));
+ properties.push(PropertyDeclaration::BorderRightStyle(dotted.clone()));
+ properties.push(PropertyDeclaration::BorderBottomStyle(solid));
+ properties.push(PropertyDeclaration::BorderLeftStyle(dotted));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(serialization, "border-style: solid dotted;");
+ }
+
+ use style::values::specified::{BorderCornerRadius, Percentage};
+
+ #[test]
+ fn border_radius_should_serialize_correctly() {
+ let mut properties = Vec::new();
+ properties.push(PropertyDeclaration::BorderTopLeftRadius(Box::new(
+ BorderCornerRadius::new(Percentage::new(0.01).into(), Percentage::new(0.05).into()),
+ )));
+ properties.push(PropertyDeclaration::BorderTopRightRadius(Box::new(
+ BorderCornerRadius::new(Percentage::new(0.02).into(), Percentage::new(0.06).into()),
+ )));
+ properties.push(PropertyDeclaration::BorderBottomRightRadius(Box::new(
+ BorderCornerRadius::new(Percentage::new(0.03).into(), Percentage::new(0.07).into()),
+ )));
+ properties.push(PropertyDeclaration::BorderBottomLeftRadius(Box::new(
+ BorderCornerRadius::new(Percentage::new(0.04).into(), Percentage::new(0.08).into()),
+ )));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(serialization, "border-radius: 1% 2% 3% 4% / 5% 6% 7% 8%;");
+ }
+ }
+
+ mod border_shorthands {
+ use super::*;
+
+ #[test]
+ fn border_top_and_color() {
+ let mut properties = Vec::new();
+ properties.push(PropertyDeclaration::BorderTopWidth(
+ BorderSideWidth::Length(Length::from_px(1.)),
+ ));
+ properties.push(PropertyDeclaration::BorderTopStyle(BorderStyle::Solid));
+ let c = Color::Numeric {
+ parsed: RGBA::new(255, 0, 0, 255),
+ authored: Some("green".to_string().into_boxed_str()),
+ };
+ properties.push(PropertyDeclaration::BorderTopColor(c));
+ let c = Color::Numeric {
+ parsed: RGBA::new(0, 255, 0, 255),
+ authored: Some("red".to_string().into_boxed_str()),
+ };
+ properties.push(PropertyDeclaration::BorderTopColor(c.clone()));
+ properties.push(PropertyDeclaration::BorderBottomColor(c.clone()));
+ properties.push(PropertyDeclaration::BorderLeftColor(c.clone()));
+ properties.push(PropertyDeclaration::BorderRightColor(c.clone()));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(
+ serialization,
+ "border-top: 1px solid red; border-color: red;"
+ );
+ }
+
+ #[test]
+ fn border_color_and_top() {
+ let mut properties = Vec::new();
+ let c = Color::Numeric {
+ parsed: RGBA::new(0, 255, 0, 255),
+ authored: Some("red".to_string().into_boxed_str()),
+ };
+ properties.push(PropertyDeclaration::BorderTopColor(c.clone()));
+ properties.push(PropertyDeclaration::BorderBottomColor(c.clone()));
+ properties.push(PropertyDeclaration::BorderLeftColor(c.clone()));
+ properties.push(PropertyDeclaration::BorderRightColor(c.clone()));
+
+ properties.push(PropertyDeclaration::BorderTopWidth(
+ BorderSideWidth::Length(Length::from_px(1.)),
+ ));
+ properties.push(PropertyDeclaration::BorderTopStyle(BorderStyle::Solid));
+ let c = Color::Numeric {
+ parsed: RGBA::new(255, 0, 0, 255),
+ authored: Some("green".to_string().into_boxed_str()),
+ };
+ properties.push(PropertyDeclaration::BorderTopColor(c));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(
+ serialization,
+ "border-color: green red red; border-top: 1px solid green;"
+ );
+ }
+
+ // we can use border-top as a base to test out the different combinations
+ // but afterwards, we only need to to one test per "directional border shorthand"
+
+ #[test]
+ fn directional_border_should_show_all_properties_when_values_are_set() {
+ let mut properties = Vec::new();
+
+ let width = BorderSideWidth::Length(Length::from_px(4f32));
+ let style = BorderStyle::Solid;
+ let color = RGBA::new(255, 0, 0, 255).into();
+
+ properties.push(PropertyDeclaration::BorderTopWidth(width));
+ properties.push(PropertyDeclaration::BorderTopStyle(style));
+ properties.push(PropertyDeclaration::BorderTopColor(color));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(serialization, "border-top: 4px solid rgb(255, 0, 0);");
+ }
+
+ fn get_border_property_values() -> (BorderSideWidth, BorderStyle, Color) {
+ (
+ BorderSideWidth::Length(Length::from_px(4f32)),
+ BorderStyle::Solid,
+ Color::currentcolor(),
+ )
+ }
+
+ #[test]
+ fn border_top_should_serialize_correctly() {
+ let mut properties = Vec::new();
+ let (width, style, color) = get_border_property_values();
+ properties.push(PropertyDeclaration::BorderTopWidth(width));
+ properties.push(PropertyDeclaration::BorderTopStyle(style));
+ properties.push(PropertyDeclaration::BorderTopColor(color));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(serialization, "border-top: 4px solid;");
+ }
+
+ #[test]
+ fn border_right_should_serialize_correctly() {
+ let mut properties = Vec::new();
+ let (width, style, color) = get_border_property_values();
+ properties.push(PropertyDeclaration::BorderRightWidth(width));
+ properties.push(PropertyDeclaration::BorderRightStyle(style));
+ properties.push(PropertyDeclaration::BorderRightColor(color));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(serialization, "border-right: 4px solid;");
+ }
+
+ #[test]
+ fn border_bottom_should_serialize_correctly() {
+ let mut properties = Vec::new();
+ let (width, style, color) = get_border_property_values();
+ properties.push(PropertyDeclaration::BorderBottomWidth(width));
+ properties.push(PropertyDeclaration::BorderBottomStyle(style));
+ properties.push(PropertyDeclaration::BorderBottomColor(color));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(serialization, "border-bottom: 4px solid;");
+ }
+
+ #[test]
+ fn border_left_should_serialize_correctly() {
+ let mut properties = Vec::new();
+ let (width, style, color) = get_border_property_values();
+ properties.push(PropertyDeclaration::BorderLeftWidth(width));
+ properties.push(PropertyDeclaration::BorderLeftStyle(style));
+ properties.push(PropertyDeclaration::BorderLeftColor(color));
+
+ let serialization = shorthand_properties_to_string(properties);
+ assert_eq!(serialization, "border-left: 4px solid;");
+ }
+
+ #[test]
+ fn border_should_serialize_correctly() {
+ // According to https://drafts.csswg.org/css-backgrounds-3/#the-border-shorthands,
+ // the ‘border’ shorthand resets ‘border-image’ to its initial value. To verify the
+ // serialization of 'border' shorthand, we need to set 'border-image' as well.
+ let block_text = "\
+ border-top: 4px solid; \
+ border-right: 4px solid; \
+ border-bottom: 4px solid; \
+ border-left: 4px solid; \
+ border-image: none;";
+
+ let block =
+ parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
+
+ let serialization = block.to_css_string();
+
+ assert_eq!(serialization, "border: 4px solid;");
+ }
+ }
+
+ mod background {
+ use super::*;
+
+ #[test]
+ fn background_should_serialize_all_available_properties_when_specified() {
+ let block_text = "\
+ background-color: rgb(255, 0, 0); \
+ background-image: url(\"http://servo/test.png\"); \
+ background-repeat: repeat-x; \
+ background-attachment: scroll; \
+ background-size: 70px 50px; \
+ background-position-x: 7px; \
+ background-position-y: bottom 4px; \
+ background-origin: border-box; \
+ background-clip: padding-box;";
+
+ let block =
+ parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
+
+ let serialization = block.to_css_string();
+
+ assert_eq!(
+ serialization,
+ "background: rgb(255, 0, 0) url(\"http://servo/test.png\") repeat-x \
+ scroll left 7px bottom 4px / 70px 50px border-box padding-box;"
+ );
+ }
+
+ #[test]
+ fn background_should_combine_origin_and_clip_properties_when_equal() {
+ let block_text = "\
+ background-color: rgb(255, 0, 0); \
+ background-image: url(\"http://servo/test.png\"); \
+ background-repeat: repeat-x; \
+ background-attachment: scroll; \
+ background-size: 70px 50px; \
+ background-position-x: 7px; \
+ background-position-y: 4px; \
+ background-origin: padding-box; \
+ background-clip: padding-box;";
+
+ let block =
+ parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
+
+ let serialization = block.to_css_string();
+
+ assert_eq!(
+ serialization,
+ "background: rgb(255, 0, 0) url(\"http://servo/test.png\") repeat-x \
+ scroll 7px 4px / 70px 50px padding-box;"
+ );
+ }
+
+ #[test]
+ fn serialize_multiple_backgrounds() {
+ let block_text = "\
+ background-color: rgb(0, 0, 255); \
+ background-image: url(\"http://servo/test.png\"), none; \
+ background-repeat: repeat-x, repeat-y; \
+ background-attachment: scroll, scroll; \
+ background-size: 70px 50px, 20px 30px; \
+ background-position-x: 7px, 70px; \
+ background-position-y: 4px, 40px; \
+ background-origin: border-box, padding-box; \
+ background-clip: padding-box, padding-box;";
+
+ let block =
+ parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
+
+ let serialization = block.to_css_string();
+
+ assert_eq!(
+ serialization, "background: \
+ url(\"http://servo/test.png\") repeat-x scroll 7px 4px / 70px 50px border-box padding-box, \
+ rgb(0, 0, 255) none repeat-y scroll 70px 40px / 20px 30px padding-box;"
+ );
+ }
+
+ #[test]
+ fn serialize_multiple_backgrounds_unequal_property_lists() {
+ // When the lengths of property values are different, the shorthand serialization
+ // should not be used. Previously the implementation cycled values if the lists were
+ // uneven. This is incorrect, in that we should serialize to a shorthand only when the
+ // lists have the same length (this affects background, transition and animation).
+ // https://github.com/servo/servo/issues/15398 )
+ // With background, the color is one exception as it should only appear once for
+ // multiple backgrounds.
+ // Below background-origin only has one value.
+ let block_text = "\
+ background-color: rgb(0, 0, 255); \
+ background-image: url(\"http://servo/test.png\"), none; \
+ background-repeat: repeat-x, repeat-y; \
+ background-attachment: scroll, scroll; \
+ background-size: 70px 50px, 20px 30px; \
+ background-position: 7px 4px, 5px 6px; \
+ background-origin: border-box; \
+ background-clip: padding-box, padding-box;";
+
+ let block =
+ parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
+
+ let serialization = block.to_css_string();
+
+ assert_eq!(serialization, block_text);
+ }
+
+ #[test]
+ fn background_position_should_be_a_valid_form_its_longhands() {
+ // If there is any longhand consisted of both keyword and position,
+ // the shorthand result should be the 4-value format.
+ let block_text = "\
+ background-position-x: 30px;\
+ background-position-y: bottom 20px;";
+ let block =
+ parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
+ let serialization = block.to_css_string();
+ assert_eq!(serialization, "background-position: left 30px bottom 20px;");
+
+ // If there is no longhand consisted of both keyword and position,
+ // the shorthand result should be the 2-value format.
+ let block_text = "\
+ background-position-x: center;\
+ background-position-y: 20px;";
+ let block =
+ parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
+ let serialization = block.to_css_string();
+ assert_eq!(serialization, "background-position: center 20px;");
+ }
+ }
+
+ mod quotes {
+ pub use super::*;
+
+ #[test]
+ fn should_serialize_none_correctly() {
+ use style::properties::longhands::quotes;
+
+ assert_roundtrip_with_context!(quotes::parse, "none");
+ }
+ }
+
+ mod animation {
+ pub use super::*;
+
+ #[test]
+ fn serialize_single_animation() {
+ let block_text = "\
+ animation-name: bounce;\
+ animation-duration: 1s;\
+ animation-timing-function: ease-in;\
+ animation-delay: 0s;\
+ animation-direction: normal;\
+ animation-fill-mode: forwards;\
+ animation-iteration-count: infinite;\
+ animation-play-state: paused;";
+
+ let block =
+ parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
+
+ let serialization = block.to_css_string();
+
+ assert_eq!(
+ serialization,
+ "animation: 1s ease-in 0s infinite normal forwards paused bounce;"
+ )
+ }
+
+ #[test]
+ fn serialize_multiple_animations() {
+ let block_text = "\
+ animation-name: bounce, roll;\
+ animation-duration: 1s, 0.2s;\
+ animation-timing-function: ease-in, linear;\
+ animation-delay: 0s, 1s;\
+ animation-direction: normal, reverse;\
+ animation-fill-mode: forwards, backwards;\
+ animation-iteration-count: infinite, 2;\
+ animation-play-state: paused, running;";
+
+ let block =
+ parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
+
+ let serialization = block.to_css_string();
+
+ assert_eq!(
+ serialization,
+ "animation: 1s ease-in 0s infinite normal forwards paused bounce, \
+ 0.2s linear 1s 2 reverse backwards running roll;"
+ );
+ }
+
+ #[test]
+ fn serialize_multiple_animations_unequal_property_lists() {
+ // When the lengths of property values are different, the shorthand serialization
+ // should not be used. Previously the implementation cycled values if the lists were
+ // uneven. This is incorrect, in that we should serialize to a shorthand only when the
+ // lists have the same length (this affects background, transition and animation).
+ // https://github.com/servo/servo/issues/15398 )
+ let block_text = "\
+ animation-name: bounce, roll, flip, jump; \
+ animation-duration: 1s, 0.2s; \
+ animation-timing-function: ease-in, linear; \
+ animation-delay: 0s, 1s, 0.5s; \
+ animation-direction: normal; \
+ animation-fill-mode: forwards, backwards; \
+ animation-iteration-count: infinite, 2; \
+ animation-play-state: paused, running;";
+
+ let block =
+ parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
+
+ let serialization = block.to_css_string();
+
+ assert_eq!(serialization, block_text);
+ }
+
+ #[test]
+ fn serialize_multiple_without_all_properties_returns_longhand() {
+ // timing function and direction are missing, so no shorthand is returned.
+ let block_text = "animation-name: bounce, roll; \
+ animation-duration: 1s, 0.2s; \
+ animation-delay: 0s, 1s; \
+ animation-fill-mode: forwards, backwards; \
+ animation-iteration-count: infinite, 2; \
+ animation-play-state: paused, running;";
+
+ let block =
+ parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
+
+ let serialization = block.to_css_string();
+
+ assert_eq!(serialization, block_text);
+ }
+ }
+
+ mod keywords {
+ pub use super::*;
+ #[test]
+ fn css_wide_keywords_should_be_parsed() {
+ let block_text = "--a:inherit;";
+ let block =
+ parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
+
+ let serialization = block.to_css_string();
+ assert_eq!(serialization, "--a: inherit;");
+ }
+
+ #[test]
+ fn non_keyword_custom_property_should_be_unparsed() {
+ let block_text = "--main-color: #06c;";
+ let block =
+ parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
+
+ let serialization = block.to_css_string();
+ assert_eq!(serialization, block_text);
+ }
+ }
+
+ mod effects {
+ pub use super::*;
+ pub use style::properties::longhands::box_shadow::SpecifiedValue as BoxShadowList;
+ pub use style::values::specified::effects::{BoxShadow, SimpleShadow};
+
+ #[test]
+ fn box_shadow_should_serialize_correctly() {
+ use style::values::specified::length::NonNegativeLength;
+
+ let mut properties = Vec::new();
+ let shadow_val = BoxShadow {
+ base: SimpleShadow {
+ color: None,
+ horizontal: Length::from_px(1f32),
+ vertical: Length::from_px(2f32),
+ blur: Some(NonNegativeLength::from_px(3f32)),
+ },
+ spread: Some(Length::from_px(4f32)),
+ inset: false,
+ };
+ let shadow_decl = BoxShadowList(vec![shadow_val]);
+ properties.push(PropertyDeclaration::BoxShadow(shadow_decl));
+ let shadow_css = "box-shadow: 1px 2px 3px 4px;";
+ let shadow =
+ parse(|c, i| Ok(parse_property_declaration_list(c, i)), shadow_css).unwrap();
+
+ assert_eq!(shadow.to_css_string(), shadow_css);
+ }
+ }
+}
diff --git a/servo/tests/unit/style/rule_tree/bench.rs b/servo/tests/unit/style/rule_tree/bench.rs
new file mode 100644
index 0000000000..52ebf2c1e2
--- /dev/null
+++ b/servo/tests/unit/style/rule_tree/bench.rs
@@ -0,0 +1,239 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use cssparser::SourceLocation;
+use rayon;
+use servo_arc::Arc;
+use servo_url::ServoUrl;
+use style::context::QuirksMode;
+use style::error_reporting::{ContextualParseError, ParseErrorReporter};
+use style::media_queries::MediaList;
+use style::properties::{longhands, Importance, PropertyDeclaration, PropertyDeclarationBlock};
+use style::rule_tree::{CascadeLevel, RuleTree, StrongRuleNode, StyleSource};
+use style::shared_lock::SharedRwLock;
+use style::stylesheets::{CssRule, Origin, Stylesheet};
+use style::thread_state::{self, ThreadState};
+use test::{self, Bencher};
+
+struct ErrorringErrorReporter;
+impl ParseErrorReporter for ErrorringErrorReporter {
+ fn report_error(&self, url: &ServoUrl, location: SourceLocation, error: ContextualParseError) {
+ panic!(
+ "CSS error: {}\t\n{}:{} {}",
+ url.as_str(),
+ location.line,
+ location.column,
+ error
+ );
+ }
+}
+
+struct AutoGCRuleTree<'a>(&'a RuleTree);
+
+impl<'a> AutoGCRuleTree<'a> {
+ fn new(r: &'a RuleTree) -> Self {
+ AutoGCRuleTree(r)
+ }
+}
+
+impl<'a> Drop for AutoGCRuleTree<'a> {
+ fn drop(&mut self) {
+ unsafe {
+ self.0.gc();
+ assert!(
+ ::std::thread::panicking() || !self.0.root().has_children_for_testing(),
+ "No rule nodes other than the root shall remain!"
+ );
+ }
+ }
+}
+
+fn parse_rules(css: &str) -> Vec<(StyleSource, CascadeLevel)> {
+ let lock = SharedRwLock::new();
+ let media = Arc::new(lock.wrap(MediaList::empty()));
+
+ let s = Stylesheet::from_str(
+ css,
+ ServoUrl::parse("http://localhost").unwrap(),
+ Origin::Author,
+ media,
+ lock,
+ None,
+ Some(&ErrorringErrorReporter),
+ QuirksMode::NoQuirks,
+ 0,
+ );
+ let guard = s.shared_lock.read();
+ let rules = s.contents.rules.read_with(&guard);
+ rules
+ .0
+ .iter()
+ .filter_map(|rule| match *rule {
+ CssRule::Style(ref style_rule) => Some((
+ StyleSource::from_rule(style_rule.clone()),
+ CascadeLevel::UserNormal,
+ )),
+ _ => None,
+ })
+ .collect()
+}
+
+fn test_insertion(rule_tree: &RuleTree, rules: Vec<(StyleSource, CascadeLevel)>) -> StrongRuleNode {
+ rule_tree.insert_ordered_rules(rules.into_iter())
+}
+
+fn test_insertion_style_attribute(
+ rule_tree: &RuleTree,
+ rules: &[(StyleSource, CascadeLevel)],
+ shared_lock: &SharedRwLock,
+) -> StrongRuleNode {
+ let mut rules = rules.to_vec();
+ rules.push((
+ StyleSource::from_declarations(Arc::new(shared_lock.wrap(
+ PropertyDeclarationBlock::with_one(
+ PropertyDeclaration::Display(longhands::display::SpecifiedValue::Block),
+ Importance::Normal,
+ ),
+ ))),
+ CascadeLevel::UserNormal,
+ ));
+ test_insertion(rule_tree, rules)
+}
+
+#[bench]
+fn bench_insertion_basic(b: &mut Bencher) {
+ let r = RuleTree::new();
+ thread_state::initialize(ThreadState::SCRIPT);
+
+ let rules_matched = parse_rules(
+ ".foo { width: 200px; } \
+ .bar { height: 500px; } \
+ .baz { display: block; }",
+ );
+
+ b.iter(|| {
+ let _gc = AutoGCRuleTree::new(&r);
+
+ for _ in 0..(4000 + 400) {
+ test::black_box(test_insertion(&r, rules_matched.clone()));
+ }
+ })
+}
+
+#[bench]
+fn bench_insertion_basic_per_element(b: &mut Bencher) {
+ let r = RuleTree::new();
+ thread_state::initialize(ThreadState::SCRIPT);
+
+ let rules_matched = parse_rules(
+ ".foo { width: 200px; } \
+ .bar { height: 500px; } \
+ .baz { display: block; }",
+ );
+
+ b.iter(|| {
+ let _gc = AutoGCRuleTree::new(&r);
+
+ test::black_box(test_insertion(&r, rules_matched.clone()));
+ });
+}
+
+#[bench]
+fn bench_expensive_insertion(b: &mut Bencher) {
+ let r = RuleTree::new();
+ thread_state::initialize(ThreadState::SCRIPT);
+
+ // This test case tests a case where you style a bunch of siblings
+ // matching the same rules, with a different style attribute each
+ // one.
+ let rules_matched = parse_rules(
+ ".foo { width: 200px; } \
+ .bar { height: 500px; } \
+ .baz { display: block; }",
+ );
+
+ let shared_lock = SharedRwLock::new();
+ b.iter(|| {
+ let _gc = AutoGCRuleTree::new(&r);
+
+ for _ in 0..(4000 + 400) {
+ test::black_box(test_insertion_style_attribute(
+ &r,
+ &rules_matched,
+ &shared_lock,
+ ));
+ }
+ });
+}
+
+#[bench]
+fn bench_insertion_basic_parallel(b: &mut Bencher) {
+ let r = RuleTree::new();
+ thread_state::initialize(ThreadState::SCRIPT);
+
+ let rules_matched = parse_rules(
+ ".foo { width: 200px; } \
+ .bar { height: 500px; } \
+ .baz { display: block; }",
+ );
+
+ b.iter(|| {
+ let _gc = AutoGCRuleTree::new(&r);
+
+ rayon::scope(|s| {
+ for _ in 0..4 {
+ s.spawn(|s| {
+ for _ in 0..1000 {
+ test::black_box(test_insertion(&r, rules_matched.clone()));
+ }
+ s.spawn(|_| {
+ for _ in 0..100 {
+ test::black_box(test_insertion(&r, rules_matched.clone()));
+ }
+ })
+ })
+ }
+ });
+ });
+}
+
+#[bench]
+fn bench_expensive_insertion_parallel(b: &mut Bencher) {
+ let r = RuleTree::new();
+ thread_state::initialize(ThreadState::SCRIPT);
+
+ let rules_matched = parse_rules(
+ ".foo { width: 200px; } \
+ .bar { height: 500px; } \
+ .baz { display: block; }",
+ );
+
+ let shared_lock = SharedRwLock::new();
+ b.iter(|| {
+ let _gc = AutoGCRuleTree::new(&r);
+
+ rayon::scope(|s| {
+ for _ in 0..4 {
+ s.spawn(|s| {
+ for _ in 0..1000 {
+ test::black_box(test_insertion_style_attribute(
+ &r,
+ &rules_matched,
+ &shared_lock,
+ ));
+ }
+ s.spawn(|_| {
+ for _ in 0..100 {
+ test::black_box(test_insertion_style_attribute(
+ &r,
+ &rules_matched,
+ &shared_lock,
+ ));
+ }
+ })
+ })
+ }
+ });
+ });
+}
diff --git a/servo/tests/unit/style/rule_tree/mod.rs b/servo/tests/unit/style/rule_tree/mod.rs
new file mode 100644
index 0000000000..6370d2640c
--- /dev/null
+++ b/servo/tests/unit/style/rule_tree/mod.rs
@@ -0,0 +1,5 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+mod bench;
diff --git a/servo/tests/unit/style/size_of.rs b/servo/tests/unit/style/size_of.rs
new file mode 100644
index 0000000000..ed435345a2
--- /dev/null
+++ b/servo/tests/unit/style/size_of.rs
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use selectors::parser::{SelectorParseError, SelectorParseErrorKind};
+use style::invalidation::element::invalidation_map::Dependency;
+use style::properties;
+
+size_of_test!(test_size_of_dependency, Dependency, 16);
+
+size_of_test!(
+ test_size_of_property_declaration,
+ properties::PropertyDeclaration,
+ 32
+);
+
+// This is huge, but we allocate it on the stack and then never move it,
+// we only pass `&mut SourcePropertyDeclaration` references around.
+size_of_test!(
+ test_size_of_parsed_declaration,
+ properties::SourcePropertyDeclaration,
+ 568
+);
+
+size_of_test!(
+ test_size_of_selector_parse_error_kind,
+ SelectorParseErrorKind,
+ 40
+);
+size_of_test!(
+ test_size_of_style_parse_error_kind,
+ ::style_traits::StyleParseErrorKind,
+ 56
+);
+size_of_test!(
+ test_size_of_value_parse_error_kind,
+ ::style_traits::ValueParseErrorKind,
+ 40
+);
+
+size_of_test!(test_size_of_selector_parse_error, SelectorParseError, 56);
+size_of_test!(
+ test_size_of_style_traits_parse_error,
+ ::style_traits::ParseError,
+ 72
+);
+size_of_test!(
+ test_size_of_value_parse_error,
+ ::style_traits::ValueParseError,
+ 56
+);
diff --git a/servo/tests/unit/style/str.rs b/servo/tests/unit/style/str.rs
new file mode 100644
index 0000000000..9445ce167c
--- /dev/null
+++ b/servo/tests/unit/style/str.rs
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use style::str::{split_html_space_chars, starts_with_ignore_ascii_case, str_join};
+
+#[test]
+pub fn split_html_space_chars_whitespace() {
+ assert!(split_html_space_chars("").collect::<Vec<_>>().is_empty());
+ assert!(
+ split_html_space_chars("\u{0020}\u{0009}\u{000a}\u{000c}\u{000d}")
+ .collect::<Vec<_>>()
+ .is_empty()
+ );
+}
+
+#[test]
+pub fn test_str_join_empty() {
+ let slice: [&str; 0] = [];
+ let actual = str_join(&slice, "-");
+ let expected = "";
+ assert_eq!(actual, expected);
+}
+
+#[test]
+pub fn test_str_join_one() {
+ let slice = ["alpha"];
+ let actual = str_join(&slice, "-");
+ let expected = "alpha";
+ assert_eq!(actual, expected);
+}
+
+#[test]
+pub fn test_str_join_many() {
+ let slice = ["", "alpha", "", "beta", "gamma", ""];
+ let actual = str_join(&slice, "-");
+ let expected = "-alpha--beta-gamma-";
+ assert_eq!(actual, expected);
+}
+
+#[test]
+pub fn test_starts_with_ignore_ascii_case_basic() {
+ assert!(starts_with_ignore_ascii_case("-webkit-", "-webkit-"));
+ assert!(starts_with_ignore_ascii_case("-webkit-foo", "-webkit-"));
+}
+
+#[test]
+pub fn test_starts_with_ignore_ascii_case_char_boundary() {
+ assert!(!starts_with_ignore_ascii_case("aaaaa💩", "-webkit-"));
+}
diff --git a/servo/tests/unit/style/stylesheets.rs b/servo/tests/unit/style/stylesheets.rs
new file mode 100644
index 0000000000..8caed3db27
--- /dev/null
+++ b/servo/tests/unit/style/stylesheets.rs
@@ -0,0 +1,564 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use cssparser::{self, SourceLocation};
+use html5ever::Namespace as NsAtom;
+use parking_lot::RwLock;
+use selectors::attr::*;
+use selectors::parser::*;
+use servo_arc::Arc;
+use servo_atoms::Atom;
+use servo_config::prefs::{PrefValue, PREFS};
+use servo_url::ServoUrl;
+use std::borrow::ToOwned;
+use std::cell::RefCell;
+use std::sync::atomic::AtomicBool;
+use style::context::QuirksMode;
+use style::error_reporting::{ContextualParseError, ParseErrorReporter};
+use style::media_queries::MediaList;
+use style::properties::longhands::{self, animation_timing_function};
+use style::properties::{CSSWideKeyword, CustomDeclaration};
+use style::properties::{CustomDeclarationValue, Importance};
+use style::properties::{PropertyDeclaration, PropertyDeclarationBlock};
+use style::shared_lock::SharedRwLock;
+use style::stylesheets::keyframes_rule::{Keyframe, KeyframePercentage, KeyframeSelector};
+use style::stylesheets::{
+ CssRule, CssRules, KeyframesRule, NamespaceRule, StyleRule, Stylesheet, StylesheetContents,
+};
+use style::stylesheets::{Namespaces, Origin};
+use style::values::computed::Percentage;
+use style::values::specified::TimingFunction;
+use style::values::specified::{LengthPercentageOrAuto, PositionComponent};
+use style::values::{CustomIdent, KeyframesName};
+
+pub fn block_from<I>(iterable: I) -> PropertyDeclarationBlock
+where
+ I: IntoIterator<Item = (PropertyDeclaration, Importance)>,
+{
+ let mut block = PropertyDeclarationBlock::new();
+ for (d, i) in iterable {
+ block.push(d, i);
+ }
+ block
+}
+
+#[test]
+fn test_parse_stylesheet() {
+ let css = r"
+ @namespace url(http://www.w3.org/1999/xhtml);
+ /* FIXME: only if scripting is enabled */
+ input[type=hidden i] {
+ display: block !important;
+ display: none !important;
+ display: inline;
+ --a: b !important;
+ --a: inherit !important;
+ --a: c;
+ }
+ html , body /**/ {
+ display: none;
+ display: block;
+ }
+ #d1 > .ok { background: blue; }
+ @keyframes foo {
+ from { width: 0% }
+ to {
+ width: 100%;
+ width: 50% !important; /* !important not allowed here */
+ animation-name: 'foo'; /* animation properties not allowed here */
+ animation-timing-function: ease; /* … except animation-timing-function */
+ }
+ }";
+ let url = ServoUrl::parse("about::test").unwrap();
+ let lock = SharedRwLock::new();
+ let media = Arc::new(lock.wrap(MediaList::empty()));
+ let stylesheet = Stylesheet::from_str(
+ css,
+ url.clone(),
+ Origin::UserAgent,
+ media,
+ lock,
+ None,
+ None,
+ QuirksMode::NoQuirks,
+ 0,
+ );
+ let mut namespaces = Namespaces::default();
+ namespaces.default = Some(ns!(html));
+ let expected = Stylesheet {
+ contents: StylesheetContents {
+ origin: Origin::UserAgent,
+ namespaces: RwLock::new(namespaces),
+ url_data: RwLock::new(url),
+ quirks_mode: QuirksMode::NoQuirks,
+ rules: CssRules::new(
+ vec![
+ CssRule::Namespace(Arc::new(stylesheet.shared_lock.wrap(NamespaceRule {
+ prefix: None,
+ url: NsAtom::from("http://www.w3.org/1999/xhtml"),
+ source_location: SourceLocation {
+ line: 1,
+ column: 19,
+ },
+ }))),
+ CssRule::Style(Arc::new(stylesheet.shared_lock.wrap(StyleRule {
+ selectors: SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::DefaultNamespace(NsAtom::from(
+ "http://www.w3.org/1999/xhtml",
+ )),
+ Component::LocalName(LocalName {
+ name: local_name!("input"),
+ lower_name: local_name!("input"),
+ }),
+ Component::AttributeInNoNamespace {
+ local_name: local_name!("type"),
+ operator: AttrSelectorOperator::Equal,
+ value: "hidden".to_owned(),
+ case_sensitivity: ParsedCaseSensitivity::AsciiCaseInsensitive,
+ never_matches: false,
+ },
+ ],
+ (0 << 20) + (1 << 10) + (1 << 0),
+ )]),
+ block: Arc::new(stylesheet.shared_lock.wrap(block_from(vec![
+ (
+ PropertyDeclaration::Display(
+ longhands::display::SpecifiedValue::None,
+ ),
+ Importance::Important,
+ ),
+ (
+ PropertyDeclaration::Custom(CustomDeclaration {
+ name: Atom::from("a"),
+ value: CustomDeclarationValue::CSSWideKeyword(
+ CSSWideKeyword::Inherit,
+ ),
+ }),
+ Importance::Important,
+ ),
+ ]))),
+ source_location: SourceLocation { line: 3, column: 9 },
+ }))),
+ CssRule::Style(Arc::new(stylesheet.shared_lock.wrap(StyleRule {
+ selectors: SelectorList::from_vec(vec![
+ Selector::from_vec(
+ vec![
+ Component::DefaultNamespace(NsAtom::from(
+ "http://www.w3.org/1999/xhtml",
+ )),
+ Component::LocalName(LocalName {
+ name: local_name!("html"),
+ lower_name: local_name!("html"),
+ }),
+ ],
+ (0 << 20) + (0 << 10) + (1 << 0),
+ ),
+ Selector::from_vec(
+ vec![
+ Component::DefaultNamespace(NsAtom::from(
+ "http://www.w3.org/1999/xhtml",
+ )),
+ Component::LocalName(LocalName {
+ name: local_name!("body"),
+ lower_name: local_name!("body"),
+ }),
+ ],
+ (0 << 20) + (0 << 10) + (1 << 0),
+ ),
+ ]),
+ block: Arc::new(stylesheet.shared_lock.wrap(block_from(vec![(
+ PropertyDeclaration::Display(longhands::display::SpecifiedValue::Block),
+ Importance::Normal,
+ )]))),
+ source_location: SourceLocation {
+ line: 11,
+ column: 9,
+ },
+ }))),
+ CssRule::Style(Arc::new(stylesheet.shared_lock.wrap(StyleRule {
+ selectors: SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::DefaultNamespace(NsAtom::from(
+ "http://www.w3.org/1999/xhtml",
+ )),
+ Component::ID(Atom::from("d1")),
+ Component::Combinator(Combinator::Child),
+ Component::DefaultNamespace(NsAtom::from(
+ "http://www.w3.org/1999/xhtml",
+ )),
+ Component::Class(Atom::from("ok")),
+ ],
+ (1 << 20) + (1 << 10) + (0 << 0),
+ )]),
+ block: Arc::new(stylesheet.shared_lock.wrap(block_from(vec![
+ (
+ PropertyDeclaration::BackgroundColor(
+ longhands::background_color::SpecifiedValue::Numeric {
+ authored: Some("blue".to_owned().into_boxed_str()),
+ parsed: cssparser::RGBA::new(0, 0, 255, 255),
+ },
+ ),
+ Importance::Normal,
+ ),
+ (
+ PropertyDeclaration::BackgroundPositionX(
+ longhands::background_position_x::SpecifiedValue(vec![
+ PositionComponent::zero(),
+ ]),
+ ),
+ Importance::Normal,
+ ),
+ (
+ PropertyDeclaration::BackgroundPositionY(
+ longhands::background_position_y::SpecifiedValue(vec![
+ PositionComponent::zero(),
+ ]),
+ ),
+ Importance::Normal,
+ ),
+ (
+ PropertyDeclaration::BackgroundRepeat(
+ longhands::background_repeat::SpecifiedValue(
+ vec![longhands::background_repeat::single_value
+ ::get_initial_specified_value()],
+ ),
+ ),
+ Importance::Normal,
+ ),
+ (
+ PropertyDeclaration::BackgroundAttachment(
+ longhands::background_attachment::SpecifiedValue(
+ vec![longhands::background_attachment::single_value
+ ::get_initial_specified_value()],
+ ),
+ ),
+ Importance::Normal,
+ ),
+ (
+ PropertyDeclaration::BackgroundImage(
+ longhands::background_image::SpecifiedValue(
+ vec![longhands::background_image::single_value
+ ::get_initial_specified_value()],
+ ),
+ ),
+ Importance::Normal,
+ ),
+ (
+ PropertyDeclaration::BackgroundSize(
+ longhands::background_size::SpecifiedValue(
+ vec![longhands::background_size::single_value
+ ::get_initial_specified_value()],
+ ),
+ ),
+ Importance::Normal,
+ ),
+ (
+ PropertyDeclaration::BackgroundOrigin(
+ longhands::background_origin::SpecifiedValue(
+ vec![longhands::background_origin::single_value
+ ::get_initial_specified_value()],
+ ),
+ ),
+ Importance::Normal,
+ ),
+ (
+ PropertyDeclaration::BackgroundClip(
+ longhands::background_clip::SpecifiedValue(
+ vec![longhands::background_clip::single_value
+ ::get_initial_specified_value()],
+ ),
+ ),
+ Importance::Normal,
+ ),
+ ]))),
+ source_location: SourceLocation {
+ line: 15,
+ column: 9,
+ },
+ }))),
+ CssRule::Keyframes(Arc::new(stylesheet.shared_lock.wrap(KeyframesRule {
+ name: KeyframesName::Ident(CustomIdent("foo".into())),
+ keyframes: vec![
+ Arc::new(stylesheet.shared_lock.wrap(Keyframe {
+ selector: KeyframeSelector::new_for_unit_testing(vec![
+ KeyframePercentage::new(0.),
+ ]),
+ block: Arc::new(stylesheet.shared_lock.wrap(block_from(vec![(
+ PropertyDeclaration::Width(LengthPercentageOrAuto::Percentage(
+ Percentage(0.),
+ )),
+ Importance::Normal,
+ )]))),
+ source_location: SourceLocation {
+ line: 17,
+ column: 13,
+ },
+ })),
+ Arc::new(stylesheet.shared_lock.wrap(Keyframe {
+ selector: KeyframeSelector::new_for_unit_testing(vec![
+ KeyframePercentage::new(1.),
+ ]),
+ block: Arc::new(stylesheet.shared_lock.wrap(block_from(vec![
+ (
+ PropertyDeclaration::Width(
+ LengthPercentageOrAuto::Percentage(Percentage(1.)),
+ ),
+ Importance::Normal,
+ ),
+ (
+ PropertyDeclaration::AnimationTimingFunction(
+ animation_timing_function::SpecifiedValue(vec![
+ TimingFunction::ease(),
+ ]),
+ ),
+ Importance::Normal,
+ ),
+ ]))),
+ source_location: SourceLocation {
+ line: 18,
+ column: 13,
+ },
+ })),
+ ],
+ vendor_prefix: None,
+ source_location: SourceLocation {
+ line: 16,
+ column: 19,
+ },
+ }))),
+ ],
+ &stylesheet.shared_lock,
+ ),
+ source_map_url: RwLock::new(None),
+ source_url: RwLock::new(None),
+ },
+ media: Arc::new(stylesheet.shared_lock.wrap(MediaList::empty())),
+ shared_lock: stylesheet.shared_lock.clone(),
+ disabled: AtomicBool::new(false),
+ };
+
+ assert_eq!(format!("{:#?}", stylesheet), format!("{:#?}", expected));
+}
+
+#[derive(Debug)]
+struct CSSError {
+ pub url: ServoUrl,
+ pub line: u32,
+ pub column: u32,
+ pub message: String,
+}
+
+struct TestingErrorReporter {
+ errors: RefCell<Vec<CSSError>>,
+}
+
+impl TestingErrorReporter {
+ pub fn new() -> Self {
+ TestingErrorReporter {
+ errors: RefCell::new(Vec::new()),
+ }
+ }
+
+ fn assert_messages_contain(&self, expected_errors: &[(u32, u32, &str)]) {
+ let errors = self.errors.borrow();
+ for (i, (error, &(line, column, message))) in errors.iter().zip(expected_errors).enumerate()
+ {
+ assert_eq!(
+ (error.line, error.column),
+ (line, column),
+ "line/column numbers of the {}th error: {:?}",
+ i + 1,
+ error.message
+ );
+ assert!(
+ error.message.contains(message),
+ "{:?} does not contain {:?}",
+ error.message,
+ message
+ );
+ }
+ if errors.len() < expected_errors.len() {
+ panic!("Missing errors: {:#?}", &expected_errors[errors.len()..]);
+ }
+ if errors.len() > expected_errors.len() {
+ panic!("Extra errors: {:#?}", &errors[expected_errors.len()..]);
+ }
+ }
+}
+
+impl ParseErrorReporter for TestingErrorReporter {
+ fn report_error(&self, url: &ServoUrl, location: SourceLocation, error: ContextualParseError) {
+ self.errors.borrow_mut().push(CSSError {
+ url: url.clone(),
+ line: location.line,
+ column: location.column,
+ message: error.to_string(),
+ })
+ }
+}
+
+#[test]
+fn test_report_error_stylesheet() {
+ PREFS.set("layout.viewport.enabled", PrefValue::Boolean(true));
+ let css = r"
+ div {
+ background-color: red;
+ display: invalid;
+ background-image: linear-gradient(0deg, black, invalid, transparent);
+ invalid: true;
+ }
+ @media (min-width: 10px invalid 1000px) {}
+ @font-face { src: url(), invalid, url(); }
+ @counter-style foo { symbols: a 0invalid b }
+ @font-feature-values Sans Sans { @foo {} @swash { foo: 1 invalid 2 } }
+ @invalid;
+ @media screen { @invalid; }
+ @supports (color: green) and invalid and (margin: 0) {}
+ @keyframes foo { from invalid {} to { margin: 0 invalid 0; } }
+ @viewport { width: 320px invalid auto; }
+ ";
+ let url = ServoUrl::parse("about::test").unwrap();
+ let error_reporter = TestingErrorReporter::new();
+
+ let lock = SharedRwLock::new();
+ let media = Arc::new(lock.wrap(MediaList::empty()));
+ Stylesheet::from_str(
+ css,
+ url.clone(),
+ Origin::UserAgent,
+ media,
+ lock,
+ None,
+ Some(&error_reporter),
+ QuirksMode::NoQuirks,
+ 5,
+ );
+
+ error_reporter.assert_messages_contain(&[
+ (
+ 8,
+ 18,
+ "Unsupported property declaration: 'display: invalid;'",
+ ),
+ (
+ 9,
+ 27,
+ "Unsupported property declaration: 'background-image:",
+ ), // FIXME: column should be around 56
+ (10, 17, "Unsupported property declaration: 'invalid: true;'"),
+ (12, 28, "Invalid media rule"),
+ (13, 30, "Unsupported @font-face descriptor declaration"),
+ // When @counter-style is supported, this should be replaced with two errors
+ (14, 19, "Invalid rule: '@counter-style "),
+ // When @font-feature-values is supported, this should be replaced with two errors
+ (15, 25, "Invalid rule: '@font-feature-values "),
+ (16, 13, "Invalid rule: '@invalid'"),
+ (17, 29, "Invalid rule: '@invalid'"),
+ (18, 34, "Invalid rule: '@supports "),
+ (19, 26, "Invalid keyframe rule: 'from invalid '"),
+ (
+ 19,
+ 52,
+ "Unsupported keyframe property declaration: 'margin: 0 invalid 0;'",
+ ),
+ (
+ 20,
+ 29,
+ "Unsupported @viewport descriptor declaration: 'width: 320px invalid auto;'",
+ ),
+ ]);
+
+ assert_eq!(error_reporter.errors.borrow()[0].url, url);
+}
+
+#[test]
+fn test_no_report_unrecognized_vendor_properties() {
+ let css = r"
+ div {
+ -o-background-color: red;
+ _background-color: red;
+ -moz-background-color: red;
+ }
+ ";
+ let url = ServoUrl::parse("about::test").unwrap();
+ let error_reporter = TestingErrorReporter::new();
+
+ let lock = SharedRwLock::new();
+ let media = Arc::new(lock.wrap(MediaList::empty()));
+ Stylesheet::from_str(
+ css,
+ url,
+ Origin::UserAgent,
+ media,
+ lock,
+ None,
+ Some(&error_reporter),
+ QuirksMode::NoQuirks,
+ 0,
+ );
+
+ error_reporter.assert_messages_contain(&[(
+ 4,
+ 31,
+ "Unsupported property declaration: '-moz-background-color: red;'",
+ )]);
+}
+
+#[test]
+fn test_source_map_url() {
+ let tests = vec![
+ ("", None),
+ (
+ "/*# sourceMappingURL=something */",
+ Some("something".to_string()),
+ ),
+ ];
+
+ for test in tests {
+ let url = ServoUrl::parse("about::test").unwrap();
+ let lock = SharedRwLock::new();
+ let media = Arc::new(lock.wrap(MediaList::empty()));
+ let stylesheet = Stylesheet::from_str(
+ test.0,
+ url.clone(),
+ Origin::UserAgent,
+ media,
+ lock,
+ None,
+ None,
+ QuirksMode::NoQuirks,
+ 0,
+ );
+ let url_opt = stylesheet.contents.source_map_url.read();
+ assert_eq!(*url_opt, test.1);
+ }
+}
+
+#[test]
+fn test_source_url() {
+ let tests = vec![
+ ("", None),
+ ("/*# sourceURL=something */", Some("something".to_string())),
+ ];
+
+ for test in tests {
+ let url = ServoUrl::parse("about::test").unwrap();
+ let lock = SharedRwLock::new();
+ let media = Arc::new(lock.wrap(MediaList::empty()));
+ let stylesheet = Stylesheet::from_str(
+ test.0,
+ url.clone(),
+ Origin::UserAgent,
+ media,
+ lock,
+ None,
+ None,
+ QuirksMode::NoQuirks,
+ 0,
+ );
+ let url_opt = stylesheet.contents.source_url.read();
+ assert_eq!(*url_opt, test.1);
+ }
+}
diff --git a/servo/tests/unit/style/stylist.rs b/servo/tests/unit/style/stylist.rs
new file mode 100644
index 0000000000..0d7511f058
--- /dev/null
+++ b/servo/tests/unit/style/stylist.rs
@@ -0,0 +1,238 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use cssparser::SourceLocation;
+use euclid::Scale;
+use euclid::Size2D;
+use selectors::parser::{AncestorHashes, Selector};
+use servo_arc::Arc;
+use servo_atoms::Atom;
+use style::context::QuirksMode;
+use style::media_queries::{Device, MediaType};
+use style::properties::{longhands, Importance};
+use style::properties::{PropertyDeclaration, PropertyDeclarationBlock};
+use style::selector_map::SelectorMap;
+use style::selector_parser::{SelectorImpl, SelectorParser};
+use style::shared_lock::SharedRwLock;
+use style::stylesheets::StyleRule;
+use style::stylist::needs_revalidation_for_testing;
+use style::stylist::{Rule, Stylist};
+use style::thread_state::{self, ThreadState};
+
+/// Helper method to get some Rules from selector strings.
+/// Each sublist of the result contains the Rules for one StyleRule.
+fn get_mock_rules(css_selectors: &[&str]) -> (Vec<Vec<Rule>>, SharedRwLock) {
+ let shared_lock = SharedRwLock::new();
+ (
+ css_selectors
+ .iter()
+ .enumerate()
+ .map(|(i, selectors)| {
+ let selectors =
+ SelectorParser::parse_author_origin_no_namespace(selectors).unwrap();
+
+ let locked = Arc::new(shared_lock.wrap(StyleRule {
+ selectors: selectors,
+ block: Arc::new(shared_lock.wrap(PropertyDeclarationBlock::with_one(
+ PropertyDeclaration::Display(longhands::display::SpecifiedValue::Block),
+ Importance::Normal,
+ ))),
+ source_location: SourceLocation { line: 0, column: 0 },
+ }));
+
+ let guard = shared_lock.read();
+ let rule = locked.read_with(&guard);
+ rule.selectors
+ .0
+ .iter()
+ .map(|s| {
+ Rule::new(
+ s.clone(),
+ AncestorHashes::new(s, QuirksMode::NoQuirks),
+ locked.clone(),
+ i as u32,
+ )
+ })
+ .collect()
+ })
+ .collect(),
+ shared_lock,
+ )
+}
+
+fn parse_selectors(selectors: &[&str]) -> Vec<Selector<SelectorImpl>> {
+ selectors
+ .iter()
+ .map(|x| {
+ SelectorParser::parse_author_origin_no_namespace(x)
+ .unwrap()
+ .0
+ .into_iter()
+ .nth(0)
+ .unwrap()
+ })
+ .collect()
+}
+
+#[test]
+fn test_revalidation_selectors() {
+ let test = parse_selectors(&[
+ // Not revalidation selectors.
+ "div",
+ "div:not(.foo)",
+ "div span",
+ "div > span",
+ // ID selectors.
+ "#foo1",
+ "#foo2::before",
+ "#foo3 > span",
+ "#foo1 > span", // FIXME(bz): This one should not be a
+ // revalidation selector, since #foo1 should be in the
+ // rule hash.
+
+ // Attribute selectors.
+ "div[foo]",
+ "div:not([foo])",
+ "div[foo = \"bar\"]",
+ "div[foo ~= \"bar\"]",
+ "div[foo |= \"bar\"]",
+ "div[foo ^= \"bar\"]",
+ "div[foo $= \"bar\"]",
+ "div[foo *= \"bar\"]",
+ "*|div[foo][bar = \"baz\"]",
+ // Non-state-based pseudo-classes.
+ "div:empty",
+ "div:first-child",
+ "div:last-child",
+ "div:only-child",
+ "div:nth-child(2)",
+ "div:nth-last-child(2)",
+ "div:nth-of-type(2)",
+ "div:nth-last-of-type(2)",
+ "div:first-of-type",
+ "div:last-of-type",
+ "div:only-of-type",
+ // Note: it would be nice to test :moz-any and the various other non-TS
+ // pseudo classes supported by gecko, but we don't have access to those
+ // in these unit tests. :-(
+
+ // Sibling combinators.
+ "span + div",
+ "span ~ div",
+ // Selectors in the ancestor chain (needed for cousin sharing).
+ "p:first-child span",
+ ])
+ .into_iter()
+ .filter(|s| needs_revalidation_for_testing(&s))
+ .collect::<Vec<_>>();
+
+ let reference = parse_selectors(&[
+ // ID selectors.
+ "#foo3 > span",
+ "#foo1 > span",
+ // Attribute selectors.
+ "div[foo]",
+ "div:not([foo])",
+ "div[foo = \"bar\"]",
+ "div[foo ~= \"bar\"]",
+ "div[foo |= \"bar\"]",
+ "div[foo ^= \"bar\"]",
+ "div[foo $= \"bar\"]",
+ "div[foo *= \"bar\"]",
+ "*|div[foo][bar = \"baz\"]",
+ // Non-state-based pseudo-classes.
+ "div:empty",
+ "div:first-child",
+ "div:last-child",
+ "div:only-child",
+ "div:nth-child(2)",
+ "div:nth-last-child(2)",
+ "div:nth-of-type(2)",
+ "div:nth-last-of-type(2)",
+ "div:first-of-type",
+ "div:last-of-type",
+ "div:only-of-type",
+ // Sibling combinators.
+ "span + div",
+ "span ~ div",
+ // Selectors in the ancestor chain (needed for cousin sharing).
+ "p:first-child span",
+ ])
+ .into_iter()
+ .collect::<Vec<_>>();
+
+ assert_eq!(test.len(), reference.len());
+ for (t, r) in test.into_iter().zip(reference.into_iter()) {
+ assert_eq!(t, r)
+ }
+}
+
+#[test]
+fn test_rule_ordering_same_specificity() {
+ let (rules_list, _) = get_mock_rules(&["a.intro", "img.sidebar"]);
+ let a = &rules_list[0][0];
+ let b = &rules_list[1][0];
+ assert!(
+ (a.specificity(), a.source_order) < ((b.specificity(), b.source_order)),
+ "The rule that comes later should win."
+ );
+}
+
+#[test]
+fn test_insert() {
+ let (rules_list, _) = get_mock_rules(&[".intro.foo", "#top"]);
+ let mut selector_map = SelectorMap::new();
+ selector_map
+ .insert(rules_list[1][0].clone(), QuirksMode::NoQuirks)
+ .expect("OOM");
+ assert_eq!(
+ 1,
+ selector_map
+ .id_hash
+ .get(&Atom::from("top"), QuirksMode::NoQuirks)
+ .unwrap()[0]
+ .source_order
+ );
+ selector_map
+ .insert(rules_list[0][0].clone(), QuirksMode::NoQuirks)
+ .expect("OOM");
+ assert_eq!(
+ 0,
+ selector_map
+ .class_hash
+ .get(&Atom::from("foo"), QuirksMode::NoQuirks)
+ .unwrap()[0]
+ .source_order
+ );
+ assert!(selector_map
+ .class_hash
+ .get(&Atom::from("intro"), QuirksMode::NoQuirks)
+ .is_none());
+}
+
+fn mock_stylist() -> Stylist {
+ let device = Device::new(
+ MediaType::screen(),
+ Size2D::new(0f32, 0f32),
+ Scale::new(1.0),
+ );
+ Stylist::new(device, QuirksMode::NoQuirks)
+}
+
+#[test]
+fn test_stylist_device_accessors() {
+ thread_state::initialize(ThreadState::LAYOUT);
+ let stylist = mock_stylist();
+ assert_eq!(stylist.device().media_type(), MediaType::screen());
+ let mut stylist_mut = mock_stylist();
+ assert_eq!(stylist_mut.device_mut().media_type(), MediaType::screen());
+}
+
+#[test]
+fn test_stylist_rule_tree_accessors() {
+ thread_state::initialize(ThreadState::LAYOUT);
+ let stylist = mock_stylist();
+ stylist.rule_tree();
+ stylist.rule_tree().root();
+}