From f399f6374b2e702ab5a978f81b989a02b8b2684c Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 2 Sep 2026 18:40:28 +0300 Subject: [PATCH] Evaluate the media query list in matchMedia against the render device CssMediaQueryList.ComputeMatched returned a constant false, so window.matchMedia(...).IsMatched answered false for every query, including "all" and the empty query, which always match. It now validates the media list against the render device from the browsing context, reusing the very same evaluation that @media rules already go through for the cascade, and falls back to DefaultRenderDevice when no device is registered - the same fallback GetComputedStyle uses. Reported in AngleSharp/AngleSharp#1307. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01NqCcJrL3MJecCPRBMQsZyC --- .../Extensions/MatchMedia.cs | 166 ++++++++++++++++++ .../Dom/Internal/CssMediaQueryList.cs | 7 +- 2 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 src/AngleSharp.Css.Tests/Extensions/MatchMedia.cs diff --git a/src/AngleSharp.Css.Tests/Extensions/MatchMedia.cs b/src/AngleSharp.Css.Tests/Extensions/MatchMedia.cs new file mode 100644 index 00000000..c07c5b0d --- /dev/null +++ b/src/AngleSharp.Css.Tests/Extensions/MatchMedia.cs @@ -0,0 +1,166 @@ +#nullable disable +namespace AngleSharp.Css.Tests.Extensions +{ + using AngleSharp.Dom; + using AngleSharp.Html.Parser; + using NUnit.Framework; + + [TestFixture] + public class MatchMediaTests + { + [Test] + public void MatchMediaWithoutAnyQueryIsMatched() + { + var window = CreateWindow(new DefaultRenderDevice { ViewPortWidth = 1000, ViewPortHeight = 800 }); + Assert.IsTrue(window.MatchMedia("").IsMatched); + } + + [Test] + public void MatchMediaAllIsMatched() + { + var window = CreateWindow(new DefaultRenderDevice { ViewPortWidth = 1000, ViewPortHeight = 800 }); + Assert.IsTrue(window.MatchMedia("all").IsMatched); + } + + [Test] + public void MatchMediaScreenIsMatchedOnScreenDevice() + { + var window = CreateWindow(new DefaultRenderDevice { Category = DeviceCategory.Screen }); + Assert.IsTrue(window.MatchMedia("screen").IsMatched); + } + + [Test] + public void MatchMediaPrintIsNotMatchedOnScreenDevice() + { + var window = CreateWindow(new DefaultRenderDevice { Category = DeviceCategory.Screen }); + Assert.IsFalse(window.MatchMedia("print").IsMatched); + } + + [Test] + public void MatchMediaPrintIsMatchedOnPrinterDevice() + { + var window = CreateWindow(new DefaultRenderDevice { Category = DeviceCategory.Printer }); + Assert.IsTrue(window.MatchMedia("print").IsMatched); + } + + [Test] + public void MatchMediaScreenIsNotMatchedOnPrinterDevice() + { + var window = CreateWindow(new DefaultRenderDevice { Category = DeviceCategory.Printer }); + Assert.IsFalse(window.MatchMedia("screen").IsMatched); + } + + [Test] + public void MatchMediaMinWidthIsMatchedForWideViewPort() + { + var window = CreateWindow(new DefaultRenderDevice { ViewPortWidth = 1000, ViewPortHeight = 800 }); + Assert.IsTrue(window.MatchMedia("(min-width: 600px)").IsMatched); + } + + [Test] + public void MatchMediaMinWidthIsNotMatchedForNarrowViewPort() + { + var window = CreateWindow(new DefaultRenderDevice { ViewPortWidth = 320, ViewPortHeight = 480 }); + Assert.IsFalse(window.MatchMedia("(min-width: 600px)").IsMatched); + } + + [Test] + public void MatchMediaMaxWidthIsMatchedForNarrowViewPort() + { + var window = CreateWindow(new DefaultRenderDevice { ViewPortWidth = 320, ViewPortHeight = 480 }); + Assert.IsTrue(window.MatchMedia("(max-width: 600px)").IsMatched); + } + + [Test] + public void MatchMediaMaxWidthIsNotMatchedForWideViewPort() + { + var window = CreateWindow(new DefaultRenderDevice { ViewPortWidth = 1000, ViewPortHeight = 800 }); + Assert.IsFalse(window.MatchMedia("(max-width: 600px)").IsMatched); + } + + [Test] + public void MatchMediaCombinedWidthRangeIsMatchedInBetween() + { + var window = CreateWindow(new DefaultRenderDevice { ViewPortWidth = 1000, ViewPortHeight = 800 }); + Assert.IsTrue(window.MatchMedia("(min-width: 600px) and (max-width: 1200px)").IsMatched); + } + + [Test] + public void MatchMediaOnlyScreenWithMinWidthIsMatchedForWideViewPort() + { + var window = CreateWindow(new DefaultRenderDevice { ViewPortWidth = 1000, ViewPortHeight = 800 }); + Assert.IsTrue(window.MatchMedia("only screen and (min-width: 600px)").IsMatched); + } + + [Test] + public void MatchMediaOnlyScreenWithMinWidthIsNotMatchedForNarrowViewPort() + { + var window = CreateWindow(new DefaultRenderDevice { ViewPortWidth = 320, ViewPortHeight = 480 }); + Assert.IsFalse(window.MatchMedia("only screen and (min-width: 600px)").IsMatched); + } + + [Test] + public void MatchMediaNotScreenIsNotMatchedOnScreenDevice() + { + var window = CreateWindow(new DefaultRenderDevice { Category = DeviceCategory.Screen }); + Assert.IsFalse(window.MatchMedia("not screen").IsMatched); + } + + [Test] + public void MatchMediaNotMinWidthIsMatchedForNarrowViewPort() + { + var window = CreateWindow(new DefaultRenderDevice { ViewPortWidth = 320, ViewPortHeight = 480 }); + Assert.IsTrue(window.MatchMedia("not (min-width: 600px)").IsMatched); + } + + [Test] + public void MatchMediaNotMinWidthIsNotMatchedForWideViewPort() + { + var window = CreateWindow(new DefaultRenderDevice { ViewPortWidth = 1000, ViewPortHeight = 800 }); + Assert.IsFalse(window.MatchMedia("not (min-width: 600px)").IsMatched); + } + + [Test] + public void MatchMediaUnknownFeatureIsNotMatched() + { + var window = CreateWindow(new DefaultRenderDevice { ViewPortWidth = 1000, ViewPortHeight = 800 }); + Assert.IsFalse(window.MatchMedia("(foo-bar: 3)").IsMatched); + } + + [Test] + public void MatchMediaMinHeightIsMatchedForTallViewPort() + { + var window = CreateWindow(new DefaultRenderDevice { ViewPortWidth = 1000, ViewPortHeight = 800 }); + Assert.IsTrue(window.MatchMedia("(min-height: 600px)").IsMatched); + } + + [Test] + public void MatchMediaWithoutRenderDeviceUsesTheDefaultDevice() + { + var context = BrowsingContext.New(Configuration.Default.WithCss()); + var window = CreateWindow(context); + Assert.IsTrue(window.MatchMedia("screen").IsMatched); + Assert.IsFalse(window.MatchMedia("print").IsMatched); + } + + [Test] + public void MatchMediaKeepsTheProvidedMediaText() + { + var window = CreateWindow(new DefaultRenderDevice { ViewPortWidth = 1000, ViewPortHeight = 800 }); + Assert.AreEqual("(min-width: 600px)", window.MatchMedia("(min-width: 600px)").MediaText); + } + + private static IWindow CreateWindow(IRenderDevice device) + { + var config = Configuration.Default.WithCss().WithRenderDevice(device); + return CreateWindow(BrowsingContext.New(config)); + } + + private static IWindow CreateWindow(IBrowsingContext context) + { + var parser = context.GetService(); + var document = parser.ParseDocument("Example"); + return document.DefaultView; + } + } +} diff --git a/src/AngleSharp.Css/Dom/Internal/CssMediaQueryList.cs b/src/AngleSharp.Css/Dom/Internal/CssMediaQueryList.cs index b57932d7..4bb56b9f 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssMediaQueryList.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssMediaQueryList.cs @@ -50,8 +50,11 @@ public CssMediaQueryList(IWindow window, IMediaList media) #region Helpers - //TODO use Validate with RenderDevice - private Boolean ComputeMatched(IWindow window) => false; + private Boolean ComputeMatched(IWindow window) + { + var device = window.Document.Context.GetService() ?? new DefaultRenderDevice(); + return _media.Validate(device); + } private void Resized(Object sender, Event ev) {