diff --git a/src/ExCSS.Tests/PreferenceMediaFeaturesTests.cs b/src/ExCSS.Tests/PreferenceMediaFeaturesTests.cs new file mode 100644 index 00000000..8f17ddf5 --- /dev/null +++ b/src/ExCSS.Tests/PreferenceMediaFeaturesTests.cs @@ -0,0 +1,63 @@ +namespace ExCSS.Tests +{ + using ExCSS; + using Xunit; + + // Registration-only coverage for the preference/interaction media features + // (Media Queries 5 / 4). Evaluation/matching is out of scope; these tests + // assert only that each feature is recognized as a typed media feature and + // does not degrade to "not all" (which is what an UnknownMediaFeature does). + public class PreferenceMediaFeaturesTests : CssConstructionFunctions + { + [Theory] + [InlineData("prefers-color-scheme", "dark")] + [InlineData("prefers-color-scheme", "light")] + [InlineData("prefers-reduced-motion", "reduce")] + [InlineData("prefers-reduced-motion", "no-preference")] + [InlineData("prefers-contrast", "more")] + [InlineData("prefers-reduced-transparency", "reduce")] + [InlineData("update", "fast")] + [InlineData("any-hover", "hover")] + [InlineData("any-pointer", "fine")] + public void RegisteredPreferenceFeatureParsesAndRoundTrips(string feature, string value) + { + var source = $"@media ({feature}: {value}) {{ h1 {{ color: green }} }}"; + var sheet = ParseStyleSheet(source); + + Assert.Equal(1, sheet.Rules.Length); + Assert.IsType(sheet.Rules[0]); + var media = (MediaRule)sheet.Rules[0]; + + // A registered feature round-trips; an unregistered one degrades to "not all". + Assert.NotEqual("not all", media.ConditionText); + Assert.Equal($"({feature}: {value})", media.Media.MediaText); + Assert.Equal(1, media.Media.Length); + Assert.Equal(1, media.Rules.Length); + } + + [Fact] + public void PreferenceFeatureCombinedWithMediaTypeParses() + { + var source = "@media screen and (prefers-color-scheme: dark) { h1 { color: green } }"; + var sheet = ParseStyleSheet(source); + + Assert.Equal(1, sheet.Rules.Length); + var media = Assert.IsType(sheet.Rules[0]); + Assert.Equal("screen and (prefers-color-scheme: dark)", media.Media.MediaText); + Assert.Equal(1, media.Rules.Length); + } + + [Fact] + public void UnregisteredFeatureStillDegradesToNotAll() + { + // Guards the assertion above: a genuinely unknown feature must degrade, + // proving the round-trip checks are meaningful. + var source = "@media (prefers-nonsense: banana) { h1 { color: green } }"; + var sheet = ParseStyleSheet(source); + + Assert.Equal(1, sheet.Rules.Length); + var media = Assert.IsType(sheet.Rules[0]); + Assert.Equal("not all", media.ConditionText); + } + } +} diff --git a/src/ExCSS/Enumerations/FeatureNames.cs b/src/ExCSS/Enumerations/FeatureNames.cs index cb264460..21cb29a5 100644 --- a/src/ExCSS/Enumerations/FeatureNames.cs +++ b/src/ExCSS/Enumerations/FeatureNames.cs @@ -44,5 +44,12 @@ public static class FeatureNames public static readonly string Hover = "hover"; public static readonly string BlockSize = "block-size"; public static readonly string InlineSize = "inline-size"; + public static readonly string PrefersColorScheme = "prefers-color-scheme"; + public static readonly string PrefersReducedMotion = "prefers-reduced-motion"; + public static readonly string PrefersContrast = "prefers-contrast"; + public static readonly string PrefersReducedTransparency = "prefers-reduced-transparency"; + public static readonly string Update = "update"; + public static readonly string AnyHover = "any-hover"; + public static readonly string AnyPointer = "any-pointer"; } } \ No newline at end of file diff --git a/src/ExCSS/Factories/MediaFeatureFactory.cs b/src/ExCSS/Factories/MediaFeatureFactory.cs index 4c5bf9c4..a7dbd140 100644 --- a/src/ExCSS/Factories/MediaFeatureFactory.cs +++ b/src/ExCSS/Factories/MediaFeatureFactory.cs @@ -63,6 +63,13 @@ internal sealed class MediaFeatureFactory {FeatureNames.Hover, () => new HoverMediaFeature()}, {FeatureNames.InlineSize, () => new SizeMediaFeature(FeatureNames.InlineSize)}, {FeatureNames.BlockSize, () => new SizeMediaFeature(FeatureNames.BlockSize)}, + {FeatureNames.PrefersColorScheme, () => new PrefersColorSchemeMediaFeature()}, + {FeatureNames.PrefersReducedMotion, () => new PrefersReducedMotionMediaFeature()}, + {FeatureNames.PrefersContrast, () => new PrefersContrastMediaFeature()}, + {FeatureNames.PrefersReducedTransparency, () => new PrefersReducedTransparencyMediaFeature()}, + {FeatureNames.Update, () => new UpdateMediaFeature()}, + {FeatureNames.AnyHover, () => new AnyHoverMediaFeature()}, + {FeatureNames.AnyPointer, () => new AnyPointerMediaFeature()}, }; #endregion diff --git a/src/ExCSS/MediaFeatures/AnyHoverMediaFeature.cs b/src/ExCSS/MediaFeatures/AnyHoverMediaFeature.cs new file mode 100644 index 00000000..e8687d40 --- /dev/null +++ b/src/ExCSS/MediaFeatures/AnyHoverMediaFeature.cs @@ -0,0 +1,11 @@ +namespace ExCSS +{ + internal sealed class AnyHoverMediaFeature : MediaFeature + { + public AnyHoverMediaFeature() : base(FeatureNames.AnyHover) + { + } + + internal override IValueConverter Converter => Converters.Any; + } +} diff --git a/src/ExCSS/MediaFeatures/AnyPointerMediaFeature.cs b/src/ExCSS/MediaFeatures/AnyPointerMediaFeature.cs new file mode 100644 index 00000000..d66f4abb --- /dev/null +++ b/src/ExCSS/MediaFeatures/AnyPointerMediaFeature.cs @@ -0,0 +1,11 @@ +namespace ExCSS +{ + internal sealed class AnyPointerMediaFeature : MediaFeature + { + public AnyPointerMediaFeature() : base(FeatureNames.AnyPointer) + { + } + + internal override IValueConverter Converter => Converters.Any; + } +} diff --git a/src/ExCSS/MediaFeatures/PrefersColorSchemeMediaFeature.cs b/src/ExCSS/MediaFeatures/PrefersColorSchemeMediaFeature.cs new file mode 100644 index 00000000..4b4e9c38 --- /dev/null +++ b/src/ExCSS/MediaFeatures/PrefersColorSchemeMediaFeature.cs @@ -0,0 +1,11 @@ +namespace ExCSS +{ + internal sealed class PrefersColorSchemeMediaFeature : MediaFeature + { + public PrefersColorSchemeMediaFeature() : base(FeatureNames.PrefersColorScheme) + { + } + + internal override IValueConverter Converter => Converters.Any; + } +} diff --git a/src/ExCSS/MediaFeatures/PrefersContrastMediaFeature.cs b/src/ExCSS/MediaFeatures/PrefersContrastMediaFeature.cs new file mode 100644 index 00000000..52741a27 --- /dev/null +++ b/src/ExCSS/MediaFeatures/PrefersContrastMediaFeature.cs @@ -0,0 +1,11 @@ +namespace ExCSS +{ + internal sealed class PrefersContrastMediaFeature : MediaFeature + { + public PrefersContrastMediaFeature() : base(FeatureNames.PrefersContrast) + { + } + + internal override IValueConverter Converter => Converters.Any; + } +} diff --git a/src/ExCSS/MediaFeatures/PrefersReducedMotionMediaFeature.cs b/src/ExCSS/MediaFeatures/PrefersReducedMotionMediaFeature.cs new file mode 100644 index 00000000..280d2f0f --- /dev/null +++ b/src/ExCSS/MediaFeatures/PrefersReducedMotionMediaFeature.cs @@ -0,0 +1,11 @@ +namespace ExCSS +{ + internal sealed class PrefersReducedMotionMediaFeature : MediaFeature + { + public PrefersReducedMotionMediaFeature() : base(FeatureNames.PrefersReducedMotion) + { + } + + internal override IValueConverter Converter => Converters.Any; + } +} diff --git a/src/ExCSS/MediaFeatures/PrefersReducedTransparencyMediaFeature.cs b/src/ExCSS/MediaFeatures/PrefersReducedTransparencyMediaFeature.cs new file mode 100644 index 00000000..c83ce389 --- /dev/null +++ b/src/ExCSS/MediaFeatures/PrefersReducedTransparencyMediaFeature.cs @@ -0,0 +1,11 @@ +namespace ExCSS +{ + internal sealed class PrefersReducedTransparencyMediaFeature : MediaFeature + { + public PrefersReducedTransparencyMediaFeature() : base(FeatureNames.PrefersReducedTransparency) + { + } + + internal override IValueConverter Converter => Converters.Any; + } +} diff --git a/src/ExCSS/MediaFeatures/UpdateMediaFeature.cs b/src/ExCSS/MediaFeatures/UpdateMediaFeature.cs new file mode 100644 index 00000000..3d5f1d55 --- /dev/null +++ b/src/ExCSS/MediaFeatures/UpdateMediaFeature.cs @@ -0,0 +1,11 @@ +namespace ExCSS +{ + internal sealed class UpdateMediaFeature : MediaFeature + { + public UpdateMediaFeature() : base(FeatureNames.Update) + { + } + + internal override IValueConverter Converter => Converters.Any; + } +}