summaryrefslogtreecommitdiffstats
path: root/servo/tests/unit/style
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /servo/tests/unit/style
parentInitial commit. (diff)
downloadfirefox-esr-upstream/115.8.0esr.tar.xz
firefox-esr-upstream/115.8.0esr.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'servo/tests/unit/style')
-rw-r--r--servo/tests/unit/style/Cargo.toml30
-rw-r--r--servo/tests/unit/style/animated_properties.rs169
-rw-r--r--servo/tests/unit/style/attr.rs88
-rw-r--r--servo/tests/unit/style/custom_properties.rs44
-rw-r--r--servo/tests/unit/style/lib.rs36
-rw-r--r--servo/tests/unit/style/logical_geometry.rs75
-rw-r--r--servo/tests/unit/style/media_queries.rs398
-rw-r--r--servo/tests/unit/style/parsing/animation.rs21
-rw-r--r--servo/tests/unit/style/parsing/background.rs104
-rw-r--r--servo/tests/unit/style/parsing/border.rs190
-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.rs91
-rw-r--r--servo/tests/unit/style/parsing/image.rs104
-rw-r--r--servo/tests/unit/style/parsing/inherited_text.rs44
-rw-r--r--servo/tests/unit/style/parsing/mod.rs126
-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.rs30
-rw-r--r--servo/tests/unit/style/parsing/supports.rs16
-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.rs37
-rw-r--r--servo/tests/unit/style/properties/mod.rs66
-rw-r--r--servo/tests/unit/style/properties/scaffolding.rs59
-rw-r--r--servo/tests/unit/style/properties/serialization.rs731
-rw-r--r--servo/tests/unit/style/rule_tree/bench.rs214
-rw-r--r--servo/tests/unit/style/rule_tree/mod.rs5
-rw-r--r--servo/tests/unit/style/size_of.rs23
-rw-r--r--servo/tests/unit/style/str.rs46
-rw-r--r--servo/tests/unit/style/stylesheets.rs431
-rw-r--r--servo/tests/unit/style/stylist.rs201
-rw-r--r--servo/tests/unit/style/viewport.rs383
33 files changed, 4032 insertions, 0 deletions
diff --git a/servo/tests/unit/style/Cargo.toml b/servo/tests/unit/style/Cargo.toml
new file mode 100644
index 0000000000..55289f3659
--- /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.31"
+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..fe0b03af87
--- /dev/null
+++ b/servo/tests/unit/style/animated_properties.rs
@@ -0,0 +1,169 @@
+/* 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..b3164c4911
--- /dev/null
+++ b/servo/tests/unit/style/attr.rs
@@ -0,0 +1,88 @@
+/* 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::{AttrValue, LengthPercentageOrAuto, parse_length};
+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..f04d4ff6b7
--- /dev/null
+++ b/servo/tests/unit/style/custom_properties.rs
@@ -0,0 +1,44 @@
+/* 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::{Name, SpecifiedValue, CustomPropertiesMap, CustomPropertiesBuilder, CssEnvironment};
+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..aa5a562d12
--- /dev/null
+++ b/servo/tests/unit/style/lib.rs
@@ -0,0 +1,36 @@
+/* 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;
+mod viewport;
diff --git a/servo/tests/unit/style/logical_geometry.rs b/servo/tests/unit/style/logical_geometry.rs
new file mode 100644
index 0000000000..8d6c06ec9a
--- /dev/null
+++ b/servo/tests/unit/style/logical_geometry.rs
@@ -0,0 +1,75 @@
+/* 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::{Size2D, Point2D, SideOffsets2D, Rect};
+use style::logical_geometry::{WritingMode, LogicalSize, LogicalPoint, LogicalMargin, LogicalRect};
+
+#[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..eaa91e6ec4
--- /dev/null
+++ b/servo/tests/unit/style/media_queries.rs
@@ -0,0 +1,398 @@
+/* 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::Atom;
+use style::context::QuirksMode;
+use style::media_queries::*;
+use style::servo::media_queries::*;
+use style::shared_lock::SharedRwLock;
+use style::stylesheets::{AllRules, Stylesheet, StylesheetInDocument, Origin, CssRule};
+use style::values::{CustomIdent, specified};
+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..a4b2d78597
--- /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::{KeyframesName, CustomIdent};
+use style::values::specified::AnimationIterationCount;
+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..7f091a5b05
--- /dev/null
+++ b/servo/tests/unit/style/parsing/background.rs
@@ -0,0 +1,104 @@
+/* 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_attachment, background_clip, background_color, background_image};
+use style::properties::longhands::{background_origin, background_position_x, background_position_y, background_repeat};
+use style::properties::longhands::background_size;
+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..3565d6ad53
--- /dev/null
+++ b/servo/tests/unit/style/parsing/border.rs
@@ -0,0 +1,190 @@
+/* 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::MaybeBoxed;
+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::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..faf1126efa
--- /dev/null
+++ b/servo/tests/unit/style/parsing/effects.rs
@@ -0,0 +1,91 @@
+/* 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..8833eaf391
--- /dev/null
+++ b/servo/tests/unit/style/parsing/image.rs
@@ -0,0 +1,104 @@
+/* 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..abcbe5d091
--- /dev/null
+++ b/servo/tests/unit/style/parsing/inherited_text.rs
@@ -0,0 +1,44 @@
+/* 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::{Length, NoCalcLength, FontRelativeLength};
+
+ 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::{NoCalcLength, LengthPercentage, FontRelativeLength};
+
+ 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..dc931633e7
--- /dev/null
+++ b/servo/tests/unit/style/parsing/mod.rs
@@ -0,0 +1,126 @@
+/* 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::{ParsingMode, ParseError};
+
+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..a1e85bd20d
--- /dev/null
+++ b/servo/tests/unit/style/parsing/selectors.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 cssparser::{Parser, ParserInput, ToCss};
+use selectors::parser::SelectorList;
+use style::selector_parser::{SelectorImpl, SelectorParser};
+use style::stylesheets::{Origin, Namespaces};
+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..15e948b73e
--- /dev/null
+++ b/servo/tests/unit/style/parsing/supports.rs
@@ -0,0 +1,16 @@
+/* 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..2d22f24459
--- /dev/null
+++ b/servo/tests/unit/style/parsing/transition_timing_function.rs
@@ -0,0 +1,37 @@
+/* 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..bfb6402692
--- /dev/null
+++ b/servo/tests/unit/style/properties/mod.rs
@@ -0,0 +1,66 @@
+/* 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::{ParsingMode, ParseError};
+
+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..7c09f51176
--- /dev/null
+++ b/servo/tests/unit/style/properties/scaffolding.rs
@@ -0,0 +1,59 @@
+/* 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::{File, remove_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..fe1d158153
--- /dev/null
+++ b/servo/tests/unit/style/properties/serialization.rs
@@ -0,0 +1,731 @@
+/* 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::{PropertyDeclaration, Importance};
+use style::properties::declaration_block::PropertyDeclarationBlock;
+use style::properties::parse_property_declaration_list;
+use style::values::RGBA;
+use style::values::specified::{BorderStyle, BorderSideWidth, Color};
+use style::values::specified::{Length, LengthPercentage, LengthPercentageOrAuto};
+use style::values::specified::NoCalcLength;
+use style::values::specified::url::SpecifiedUrl;
+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..d7b024a1b4
--- /dev/null
+++ b/servo/tests/unit/style/rule_tree/bench.rs
@@ -0,0 +1,214 @@
+/* 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::{ParseErrorReporter, ContextualParseError};
+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::{Origin, Stylesheet, CssRule};
+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..744d3596ac
--- /dev/null
+++ b/servo/tests/unit/style/size_of.rs
@@ -0,0 +1,23 @@
+/* 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..149b3f231f
--- /dev/null
+++ b/servo/tests/unit/style/str.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 style::str::{split_html_space_chars, str_join, starts_with_ignore_ascii_case};
+
+#[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..e612a39d11
--- /dev/null
+++ b/servo/tests/unit/style/stylesheets.rs
@@ -0,0 +1,431 @@
+/* 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::{PREFS, PrefValue};
+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::{ParseErrorReporter, ContextualParseError};
+use style::media_queries::MediaList;
+use style::properties::{CSSWideKeyword, CustomDeclaration};
+use style::properties::{CustomDeclarationValue, Importance};
+use style::properties::{PropertyDeclaration, PropertyDeclarationBlock};
+use style::properties::longhands::{self, animation_timing_function};
+use style::shared_lock::SharedRwLock;
+use style::stylesheets::{Origin, Namespaces};
+use style::stylesheets::{Stylesheet, StylesheetContents, NamespaceRule, CssRule, CssRules, StyleRule, KeyframesRule};
+use style::stylesheets::keyframes_rule::{Keyframe, KeyframeSelector, KeyframePercentage};
+use style::values::{KeyframesName, CustomIdent};
+use style::values::computed::Percentage;
+use style::values::specified::{LengthPercentageOrAuto, PositionComponent};
+use style::values::specified::TimingFunction;
+
+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..5a4d3be244
--- /dev/null
+++ b/servo/tests/unit/style/stylist.rs
@@ -0,0 +1,201 @@
+/* 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::{PropertyDeclarationBlock, PropertyDeclaration};
+use style::properties::{longhands, Importance};
+use style::selector_map::SelectorMap;
+use style::selector_parser::{SelectorImpl, SelectorParser};
+use style::shared_lock::SharedRwLock;
+use style::stylesheets::StyleRule;
+use style::stylist::{Stylist, Rule};
+use style::stylist::needs_revalidation_for_testing;
+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();
+}
diff --git a/servo/tests/unit/style/viewport.rs b/servo/tests/unit/style/viewport.rs
new file mode 100644
index 0000000000..432f931329
--- /dev/null
+++ b/servo/tests/unit/style/viewport.rs
@@ -0,0 +1,383 @@
+/* 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 euclid::Scale;
+use euclid::Size2D;
+use servo_arc::Arc;
+use servo_config::prefs::{PREFS, PrefValue};
+use servo_url::ServoUrl;
+use style::context::QuirksMode;
+use style::media_queries::{Device, MediaList, MediaType};
+use style::parser::ParserContext;
+use style::shared_lock::{SharedRwLock, StylesheetGuards};
+use style::stylesheets::{CssRuleType, Stylesheet, StylesheetInDocument, Origin};
+use style::stylesheets::viewport_rule::*;
+use style::values::specified::LengthPercentageOrAuto::{self, Auto};
+use style::values::specified::NoCalcLength::{self, ViewportPercentage};
+use style::values::specified::ViewportPercentageLength::Vw;
+use style_traits::{ParsingMode, PinchZoomFactor};
+use style_traits::viewport::*;
+
+macro_rules! stylesheet {
+ ($css:expr, $origin:ident) => {
+ stylesheet!($css, $origin, SharedRwLock::new())
+ };
+ ($css:expr, $origin:ident, $shared_lock:expr) => {
+ Arc::new(Stylesheet::from_str(
+ $css,
+ ServoUrl::parse("http://localhost").unwrap(),
+ Origin::$origin,
+ Arc::new($shared_lock.wrap(MediaList::empty())),
+ $shared_lock,
+ None,
+ None,
+ QuirksMode::NoQuirks,
+ 0
+ ))
+ }
+}
+
+fn test_viewport_rule<F>(css: &str,
+ device: &Device,
+ callback: F)
+ where F: Fn(&Vec<ViewportDescriptorDeclaration>, &str)
+{
+ PREFS.set("layout.viewport.enabled", PrefValue::Boolean(true));
+ let stylesheet = stylesheet!(css, Author);
+ let mut rule_count = 0;
+ stylesheet.effective_viewport_rules(&device, &stylesheet.shared_lock.read(), |rule| {
+ rule_count += 1;
+ callback(&rule.declarations, css);
+ });
+ assert!(rule_count > 0);
+}
+
+fn test_meta_viewport<F>(meta: &str, callback: F)
+ where F: Fn(&Vec<ViewportDescriptorDeclaration>, &str)
+{
+ if let Some(mut rule) = ViewportRule::from_meta(meta) {
+ // from_meta uses a hash-map to collect the declarations, so we need to
+ // sort them in a stable order for the tests
+ rule.declarations.sort_by(|a, b| {
+ let a = a.descriptor.discriminant_value();
+ let b = b.descriptor.discriminant_value();
+ a.cmp(&b)
+ });
+
+ callback(&rule.declarations, meta);
+ } else {
+ panic!("no @viewport rule for {}", meta);
+ }
+}
+
+macro_rules! assert_decl_len {
+ ($declarations:ident == 1) => {
+ assert_eq!($declarations.len(), 1,
+ "expected 1 declaration; have {}: {:?})",
+ $declarations.len(), $declarations)
+ };
+ ($declarations:ident == $len:expr) => {
+ assert_eq!($declarations.len(), $len,
+ "expected {} declarations; have {}: {:?})",
+ $len, $declarations.len(), $declarations)
+ }
+}
+
+macro_rules! viewport_length {
+ ($value:expr, px) => {
+ ViewportLength::Specified(LengthPercentageOrAuto::Length(NoCalcLength::from_px($value)))
+ };
+ ($value:expr, vw) => {
+ ViewportLength::Specified(LengthPercentageOrAuto::Length(ViewportPercentage(Vw($value))))
+ }
+}
+
+#[test]
+fn empty_viewport_rule() {
+ let device = Device::new(MediaType::screen(), Size2D::new(800., 600.), Scale::new(1.0));
+
+ test_viewport_rule("@viewport {}", &device, |declarations, css| {
+ println!("{}", css);
+ assert_decl_len!(declarations == 0);
+ });
+}
+
+macro_rules! assert_decl_eq {
+ ($d:expr, $origin:ident, $expected:ident: $value:expr) => {{
+ assert_eq!($d.origin, Origin::$origin);
+ assert_eq!($d.descriptor, ViewportDescriptor::$expected($value));
+ assert_eq!($d.important, false, "descriptor should not be !important");
+ }};
+ ($d:expr, $origin:ident, $expected:ident: $value:expr, !important) => {{
+ assert_eq!($d.origin, Origin::$origin);
+ assert_eq!($d.descriptor, ViewportDescriptor::$expected($value));
+ assert_eq!($d.important, true, "descriptor should be !important");
+ }};
+}
+
+#[test]
+fn simple_viewport_rules() {
+ let device = Device::new(MediaType::screen(), Size2D::new(800., 600.), Scale::new(1.0));
+
+ test_viewport_rule("@viewport { width: auto; height: auto;\
+ zoom: auto; min-zoom: 0; max-zoom: 200%;\
+ user-zoom: zoom; orientation: auto; }",
+ &device, |declarations, css| {
+ println!("{}", css);
+ assert_decl_len!(declarations == 9);
+ assert_decl_eq!(&declarations[0], Author, MinWidth: ViewportLength::Specified(Auto));
+ assert_decl_eq!(&declarations[1], Author, MaxWidth: ViewportLength::Specified(Auto));
+ assert_decl_eq!(&declarations[2], Author, MinHeight: ViewportLength::Specified(Auto));
+ assert_decl_eq!(&declarations[3], Author, MaxHeight: ViewportLength::Specified(Auto));
+ assert_decl_eq!(&declarations[4], Author, Zoom: Zoom::Auto);
+ assert_decl_eq!(&declarations[5], Author, MinZoom: Zoom::Number(0.));
+ assert_decl_eq!(&declarations[6], Author, MaxZoom: Zoom::Percentage(2.));
+ assert_decl_eq!(&declarations[7], Author, UserZoom: UserZoom::Zoom);
+ assert_decl_eq!(&declarations[8], Author, Orientation: Orientation::Auto);
+ });
+
+ test_viewport_rule("@viewport { min-width: 200px; max-width: auto;\
+ min-height: 200px; max-height: auto; }",
+ &device, |declarations, css| {
+ println!("{}", css);
+ assert_decl_len!(declarations == 4);
+ assert_decl_eq!(&declarations[0], Author, MinWidth: viewport_length!(200., px));
+ assert_decl_eq!(&declarations[1], Author, MaxWidth: ViewportLength::Specified(Auto));
+ assert_decl_eq!(&declarations[2], Author, MinHeight: viewport_length!(200., px));
+ assert_decl_eq!(&declarations[3], Author, MaxHeight: ViewportLength::Specified(Auto));
+ });
+}
+
+#[test]
+fn simple_meta_viewport_contents() {
+ test_meta_viewport("width=500, height=600", |declarations, meta| {
+ println!("{}", meta);
+ assert_decl_len!(declarations == 4);
+ assert_decl_eq!(&declarations[0], Author, MinWidth: ViewportLength::ExtendToZoom);
+ assert_decl_eq!(&declarations[1], Author, MaxWidth: viewport_length!(500., px));
+ assert_decl_eq!(&declarations[2], Author, MinHeight: ViewportLength::ExtendToZoom);
+ assert_decl_eq!(&declarations[3], Author, MaxHeight: viewport_length!(600., px));
+ });
+
+ test_meta_viewport("initial-scale=1.0", |declarations, meta| {
+ println!("{}", meta);
+ assert_decl_len!(declarations == 3);
+ assert_decl_eq!(&declarations[0], Author, MinWidth: ViewportLength::ExtendToZoom);
+ assert_decl_eq!(&declarations[1], Author, MaxWidth: ViewportLength::ExtendToZoom);
+ assert_decl_eq!(&declarations[2], Author, Zoom: Zoom::Number(1.));
+ });
+
+ test_meta_viewport("initial-scale=2.0, height=device-width", |declarations, meta| {
+ println!("{}", meta);
+ assert_decl_len!(declarations == 5);
+ assert_decl_eq!(&declarations[0], Author, MinWidth: ViewportLength::Specified(Auto));
+ assert_decl_eq!(&declarations[1], Author, MaxWidth: ViewportLength::Specified(Auto));
+ assert_decl_eq!(&declarations[2], Author, MinHeight: ViewportLength::ExtendToZoom);
+ assert_decl_eq!(&declarations[3], Author, MaxHeight: viewport_length!(100., vw));
+ assert_decl_eq!(&declarations[4], Author, Zoom: Zoom::Number(2.));
+ });
+
+ test_meta_viewport("width=480, initial-scale=2.0, user-scalable=1", |declarations, meta| {
+ println!("{}", meta);
+ assert_decl_len!(declarations == 4);
+ assert_decl_eq!(&declarations[0], Author, MinWidth: ViewportLength::ExtendToZoom);
+ assert_decl_eq!(&declarations[1], Author, MaxWidth: viewport_length!(480., px));
+ assert_decl_eq!(&declarations[2], Author, Zoom: Zoom::Number(2.));
+ assert_decl_eq!(&declarations[3], Author, UserZoom: UserZoom::Zoom);
+ });
+}
+
+#[test]
+fn cascading_within_viewport_rule() {
+ let device = Device::new(MediaType::screen(), Size2D::new(800., 600.), Scale::new(1.0));
+
+ // normal order of appearance
+ test_viewport_rule("@viewport { min-width: 200px; min-width: auto; }",
+ &device, |declarations, css| {
+ println!("{}", css);
+ assert_decl_len!(declarations == 1);
+ assert_decl_eq!(&declarations[0], Author, MinWidth: ViewportLength::Specified(Auto));
+ });
+
+ // !important order of appearance
+ test_viewport_rule("@viewport { min-width: 200px !important; min-width: auto !important; }",
+ &device, |declarations, css| {
+ println!("{}", css);
+ assert_decl_len!(declarations == 1);
+ assert_decl_eq!(&declarations[0], Author, MinWidth: ViewportLength::Specified(Auto), !important);
+ });
+
+ // !important vs normal
+ test_viewport_rule("@viewport { min-width: auto !important; min-width: 200px; }",
+ &device, |declarations, css| {
+ println!("{}", css);
+ assert_decl_len!(declarations == 1);
+ assert_decl_eq!(&declarations[0], Author, MinWidth: ViewportLength::Specified(Auto), !important);
+ });
+
+ // normal longhands vs normal shorthand
+ test_viewport_rule("@viewport { min-width: 200px; max-width: 200px; width: auto; }",
+ &device, |declarations, css| {
+ println!("{}", css);
+ assert_decl_len!(declarations == 2);
+ assert_decl_eq!(&declarations[0], Author, MinWidth: ViewportLength::Specified(Auto));
+ assert_decl_eq!(&declarations[1], Author, MaxWidth: ViewportLength::Specified(Auto));
+ });
+
+ // normal shorthand vs normal longhands
+ test_viewport_rule("@viewport { width: 200px; min-width: auto; max-width: auto; }",
+ &device, |declarations, css| {
+ println!("{}", css);
+ assert_decl_len!(declarations == 2);
+ assert_decl_eq!(&declarations[0], Author, MinWidth: ViewportLength::Specified(Auto));
+ assert_decl_eq!(&declarations[1], Author, MaxWidth: ViewportLength::Specified(Auto));
+ });
+
+ // one !important longhand vs normal shorthand
+ test_viewport_rule("@viewport { min-width: auto !important; width: 200px; }",
+ &device, |declarations, css| {
+ println!("{}", css);
+ assert_decl_len!(declarations == 2);
+ assert_decl_eq!(&declarations[0], Author, MinWidth: ViewportLength::Specified(Auto), !important);
+ assert_decl_eq!(&declarations[1], Author, MaxWidth: viewport_length!(200., px));
+ });
+
+ // both !important longhands vs normal shorthand
+ test_viewport_rule("@viewport { min-width: auto !important; max-width: auto !important; width: 200px; }",
+ &device, |declarations, css| {
+ println!("{}", css);
+ assert_decl_len!(declarations == 2);
+ assert_decl_eq!(&declarations[0], Author, MinWidth: ViewportLength::Specified(Auto), !important);
+ assert_decl_eq!(&declarations[1], Author, MaxWidth: ViewportLength::Specified(Auto), !important);
+ });
+}
+
+#[test]
+fn multiple_stylesheets_cascading() {
+ PREFS.set("layout.viewport.enabled", PrefValue::Boolean(true));
+ let device = Device::new(MediaType::screen(), Size2D::new(800., 600.), Scale::new(1.0));
+ let shared_lock = SharedRwLock::new();
+ let stylesheets = vec![
+ stylesheet!("@viewport { min-width: 100px; min-height: 100px; zoom: 1; }",
+ UserAgent,
+ shared_lock.clone()),
+ stylesheet!("@viewport { min-width: 200px; min-height: 200px; }",
+ User, shared_lock.clone()),
+ stylesheet!("@viewport { min-width: 300px; }",
+ Author, shared_lock.clone())
+ ];
+
+ let declarations = Cascade::from_stylesheets(
+ stylesheets.iter().map(|s| (&**s, Origin::Author)),
+ &StylesheetGuards::same(&shared_lock.read()),
+ &device,
+ ).finish();
+ assert_decl_len!(declarations == 3);
+ assert_decl_eq!(&declarations[0], UserAgent, Zoom: Zoom::Number(1.));
+ assert_decl_eq!(&declarations[1], User, MinHeight: viewport_length!(200., px));
+ assert_decl_eq!(&declarations[2], Author, MinWidth: viewport_length!(300., px));
+
+ let stylesheets = vec![
+ stylesheet!("@viewport { min-width: 100px !important; }",
+ UserAgent, shared_lock.clone()),
+ stylesheet!("@viewport { min-width: 200px !important; min-height: 200px !important; }",
+ User, shared_lock.clone()),
+ stylesheet!("@viewport { min-width: 300px !important; min-height: 300px !important; zoom: 3 !important; }",
+ Author, shared_lock.clone())
+ ];
+ let declarations = Cascade::from_stylesheets(
+ stylesheets.iter().map(|s| (&**s, Origin::Author)),
+ &StylesheetGuards::same(&shared_lock.read()),
+ &device,
+ ).finish();
+ assert_decl_len!(declarations == 3);
+ assert_decl_eq!(&declarations[0], UserAgent, MinWidth: viewport_length!(100., px), !important);
+ assert_decl_eq!(&declarations[1], User, MinHeight: viewport_length!(200., px), !important);
+ assert_decl_eq!(&declarations[2], Author, Zoom: Zoom::Number(3.), !important);
+}
+
+#[test]
+fn constrain_viewport() {
+ let url = ServoUrl::parse("http://localhost").unwrap();
+ let context = ParserContext::new(
+ Origin::Author,
+ &url,
+ Some(CssRuleType::Viewport),
+ ParsingMode::DEFAULT,
+ QuirksMode::NoQuirks,
+ None,
+ None,
+ );
+
+ macro_rules! from_css {
+ ($css:expr) => {
+ &ViewportRule::parse(&context, &mut Parser::new(&mut $css)).unwrap()
+ }
+ }
+
+ let initial_viewport = Size2D::new(800., 600.);
+ let device = Device::new(MediaType::screen(), initial_viewport, Scale::new(1.0));
+ let mut input = ParserInput::new("");
+ assert_eq!(ViewportConstraints::maybe_new(&device, from_css!(input), QuirksMode::NoQuirks), None);
+
+ let mut input = ParserInput::new("width: 320px auto");
+ assert_eq!(ViewportConstraints::maybe_new(&device, from_css!(input), QuirksMode::NoQuirks),
+ Some(ViewportConstraints {
+ size: initial_viewport,
+
+ initial_zoom: PinchZoomFactor::new(1.),
+ min_zoom: None,
+ max_zoom: None,
+
+ user_zoom: UserZoom::Zoom,
+ orientation: Orientation::Auto
+ }));
+
+ let mut input = ParserInput::new("width: 320px auto");
+ assert_eq!(ViewportConstraints::maybe_new(&device, from_css!(input), QuirksMode::NoQuirks),
+ Some(ViewportConstraints {
+ size: initial_viewport,
+
+ initial_zoom: PinchZoomFactor::new(1.),
+ min_zoom: None,
+ max_zoom: None,
+
+ user_zoom: UserZoom::Zoom,
+ orientation: Orientation::Auto
+ }));
+
+ let mut input = ParserInput::new("width: 800px; height: 600px;\
+ zoom: 1;\
+ user-zoom: zoom;\
+ orientation: auto;");
+ assert_eq!(ViewportConstraints::maybe_new(&device,
+ from_css!(input),
+ QuirksMode::NoQuirks),
+ Some(ViewportConstraints {
+ size: initial_viewport,
+
+ initial_zoom: PinchZoomFactor::new(1.),
+ min_zoom: None,
+ max_zoom: None,
+
+ user_zoom: UserZoom::Zoom,
+ orientation: Orientation::Auto
+ }));
+
+ let initial_viewport = Size2D::new(200., 150.);
+ let device = Device::new(MediaType::screen(), initial_viewport, Scale::new(1.0));
+ let mut input = ParserInput::new("width: 320px auto");
+ assert_eq!(ViewportConstraints::maybe_new(&device, from_css!(input), QuirksMode::NoQuirks),
+ Some(ViewportConstraints {
+ size: Size2D::new(320., 240.),
+
+ initial_zoom: PinchZoomFactor::new(1.),
+ min_zoom: None,
+ max_zoom: None,
+
+ user_zoom: UserZoom::Zoom,
+ orientation: Orientation::Auto
+ }));
+}