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