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