diff --git a/MIGRATION.md b/MIGRATION.md index 02f944288..1c1c3a358 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -82,3 +82,26 @@ The `TestServiceProvider` class has been renamed to `BunitTestServiceProvider`. ## `DisposeComponents` is now asynchronous and called `DisposeComponentsAsync` `DisposeComponentsAsync` allows to await `DisposeAsync` of components under test. If you used `DisposeComponents`, you should replace it with `DisposeComponentsAsync`. + +## `IRefreshableElementCollection` was removed + +The `IRefreshableElementCollection` interface has been removed. With this the `FindAll` method does not accept a `bool enableRefresh` parameter anymore. Code like this: + +```csharp +var items = cut.FindAll("li", enableRefresh: true); + +cut.Find("button").Click(); // Some action that causes items to change + +Assert.Equal(3, items.Count); +``` + +Should be changed to this: + +```csharp +var items = cut.FindAll("li"); + +cut.Find("button").Click(); // Some action that causes items to change + +items = cut.FindAll("li"); // Re-query the items +Assert.Equal(3, items.Count); +``` \ No newline at end of file diff --git a/docs/site/docs/verification/verify-markup.md b/docs/site/docs/verification/verify-markup.md index 11717b9a5..5628fa2b3 100644 --- a/docs/site/docs/verification/verify-markup.md +++ b/docs/site/docs/verification/verify-markup.md @@ -37,9 +37,9 @@ bUnit supports multiple different ways of searching and querying the rendered HT - `FindByLabelText(string labelText)` that takes a text string used to label an input element and returns an `IElement` as output, or throws an exception if none are found (this is included in the experimental library [bunit.web.query](https://www.nuget.org/packages/bunit.web.query)). Use this method when possible compared to the generic `Find` and `FindAll` methods. - [`Find(string cssSelector)`](xref:Bunit.RenderedComponentExtensions.Find``1(Bunit.IRenderedComponent{``0},System.String)) takes a "CSS selector" as input and returns an `IElement` as output, or throws an exception if none are found. -- [`FindAll(string cssSelector)`](xref:Bunit.RenderedComponentExtensions.FindAll``1(Bunit.IRenderedComponent{``0},System.String,System.Boolean)) takes a "CSS selector" as input and returns a list of `IElement` elements. +- [`FindAll(string cssSelector)`](xref:Bunit.RenderedComponentExtensions.FindAll``1(Bunit.IRenderedComponent{``0},System.String)) takes a "CSS selector" as input and returns a list of `IElement` elements. -Let's see some examples of using the [`Find(string cssSelector)`](xref:Bunit.RenderedComponentExtensions.Find``1(Bunit.IRenderedComponent{``0},System.String)) and [`FindAll(string cssSelector)`](xref:Bunit.RenderedComponentExtensions.FindAll``1(Bunit.IRenderedComponent{``0},System.String,System.Boolean)) methods to query the `` component listed below. +Let's see some examples of using the [`Find(string cssSelector)`](xref:Bunit.RenderedComponentExtensions.Find``1(Bunit.IRenderedComponent{``0},System.String)) and [`FindAll(string cssSelector)`](xref:Bunit.RenderedComponentExtensions.FindAll``1(Bunit.IRenderedComponent{``0},System.String)) methods to query the `` component listed below. [!code-razor[FancyTable.razor](../../../samples/components/FancyTable.razor)] @@ -59,10 +59,6 @@ However, that does not apply to elements that are found by traversing the DOM tr As a result of this, it is always recommended to use the [`Find(string cssSelector)`](xref:Bunit.RenderedComponentExtensions.Find``1(Bunit.IRenderedComponent{``0},System.String)) method when searching for a single element. Alternatively, always reissue the query whenever you need the element. -#### Auto-refreshable FindAll() queries - -The [`FindAll(string cssSelector, bool enableAutoRefresh = false)`](xref:Bunit.RenderedComponentExtensions.FindAll``1(Bunit.IRenderedComponent{``0},System.String,System.Boolean)) method has an optional parameter, `enableAutoRefresh`, which when set to `true` will return a collection of `IElement`. This automatically refreshes itself when the component the elements came from is re-rendered. - ## Semantic comparison of markup Working with raw markup only works well with very simple output, but even then you have to sanitize it to get stable tests. A much better approach is to use the semantic HTML comparer that comes with bUnit. diff --git a/src/bunit/Extensions/IRefreshableElementCollection.cs b/src/bunit/Extensions/IRefreshableElementCollection.cs deleted file mode 100644 index b98c0e093..000000000 --- a/src/bunit/Extensions/IRefreshableElementCollection.cs +++ /dev/null @@ -1,24 +0,0 @@ -using AngleSharp.Dom; - -namespace Bunit; - -/// -/// Represents a collection, which queries and finds its -/// elements in an , based on a CSS selector. -/// The collection can be refreshed either manually or automatically. -/// -/// The type of in the collection. -public interface IRefreshableElementCollection : IReadOnlyList - where T : IElement -{ - /// - /// Gets or sets a value indicating whether the collection automatically refreshes when the - /// changes. - /// - bool EnableAutoRefresh { get; set; } - - /// - /// Trigger a refresh of the elements in the collection, by querying the rendered fragments DOM tree. - /// - void Refresh(); -} diff --git a/src/bunit/Extensions/Internal/RefreshableElementCollection.cs b/src/bunit/Extensions/Internal/RefreshableElementCollection.cs deleted file mode 100644 index f6041fe2a..000000000 --- a/src/bunit/Extensions/Internal/RefreshableElementCollection.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Collections; -using System.Diagnostics; -using AngleSharp.Dom; - -namespace Bunit; - -[DebuggerDisplay("Selector={cssSelector}, AutoRefresh={enableAutoRefresh}")] -internal sealed class RefreshableElementCollection : IRefreshableElementCollection -{ - private readonly IRenderedComponent renderedComponent; - private readonly string cssSelector; - private IHtmlCollection elements; - private bool enableAutoRefresh; - - public bool EnableAutoRefresh - { - get => enableAutoRefresh; - set - { - if (ShouldEnable(value)) - { - renderedComponent.OnMarkupUpdated += RefreshInternal; - } - - if (ShouldDisable(value)) - { - renderedComponent.OnMarkupUpdated -= RefreshInternal; - } - - enableAutoRefresh = value; - } - } - - private bool ShouldDisable(bool value) => !value && enableAutoRefresh; - - private bool ShouldEnable(bool value) => value && !enableAutoRefresh; - - internal RefreshableElementCollection(IRenderedComponent renderedComponent, string cssSelector) - { - this.renderedComponent = renderedComponent; - this.cssSelector = cssSelector; - elements = renderedComponent.Nodes.QuerySelectorAll(cssSelector); - } - - public void Refresh() => RefreshInternal(this, EventArgs.Empty); - - public IElement this[int index] => elements[index]; - - public int Count => elements.Length; - - public IEnumerator GetEnumerator() => elements.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - private void RefreshInternal(object? sender, EventArgs args) - { - elements = renderedComponent.Nodes.QuerySelectorAll(cssSelector); - } -} diff --git a/src/bunit/Extensions/RenderedComponentExtensions.cs b/src/bunit/Extensions/RenderedComponentExtensions.cs index dfbefd584..199ae3316 100644 --- a/src/bunit/Extensions/RenderedComponentExtensions.cs +++ b/src/bunit/Extensions/RenderedComponentExtensions.cs @@ -36,13 +36,13 @@ public static IElement Find(this IRenderedComponent rend /// /// The rendered fragment to search. /// The group of selectors to use. - /// If true, the returned will automatically refresh its s whenever the changes. - /// An , that can be refreshed to execute the search again. - public static IRefreshableElementCollection FindAll(this IRenderedComponent renderedComponent, string cssSelector, bool enableAutoRefresh = false) + /// An , that can be refreshed to execute the search again. + public static IReadOnlyList FindAll(this IRenderedComponent renderedComponent, string cssSelector) where TComponent : IComponent { ArgumentNullException.ThrowIfNull(renderedComponent); - return new RefreshableElementCollection((IRenderedComponent)renderedComponent, cssSelector) { EnableAutoRefresh = enableAutoRefresh }; + + return renderedComponent.Nodes.QuerySelectorAll(cssSelector).ToArray(); } /// diff --git a/src/bunit/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.WaitForElement.cs b/src/bunit/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.WaitForElement.cs index 912dece39..fc32ddcd2 100644 --- a/src/bunit/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.WaitForElement.cs +++ b/src/bunit/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.WaitForElement.cs @@ -40,8 +40,8 @@ public static IElement WaitForElement(this IRenderedComponentThe render fragment or component find the matching element in. /// The CSS selector to use to search for elements. /// Thrown if no elements is found matching the within the default timeout. - /// The . - public static IRefreshableElementCollection WaitForElements(this IRenderedComponent renderedComponent, string cssSelector) + /// The . + public static IReadOnlyList WaitForElements(this IRenderedComponent renderedComponent, string cssSelector) where TComponent : IComponent => WaitForElementsCore(renderedComponent, cssSelector, matchElementCount: null, timeout: null); /// @@ -52,8 +52,8 @@ public static IRefreshableElementCollection WaitForElementsThe CSS selector to use to search for elements. /// The exact number of elements to that the should match. /// Thrown if no elements is found matching the within the default timeout. - /// The . - public static IRefreshableElementCollection WaitForElements(this IRenderedComponent renderedComponent, string cssSelector, int matchElementCount) + /// The . + public static IReadOnlyList WaitForElements(this IRenderedComponent renderedComponent, string cssSelector, int matchElementCount) where TComponent : IComponent => WaitForElementsCore(renderedComponent, cssSelector, matchElementCount: matchElementCount, timeout: null); /// @@ -64,8 +64,8 @@ public static IRefreshableElementCollection WaitForElementsThe CSS selector to use to search for elements. /// The maximum time to wait for elements to appear. /// Thrown if no elements is found matching the within the default timeout. - /// The . - public static IRefreshableElementCollection WaitForElements(this IRenderedComponent renderedComponent, string cssSelector, TimeSpan timeout) + /// The . + public static IReadOnlyList WaitForElements(this IRenderedComponent renderedComponent, string cssSelector, TimeSpan timeout) where TComponent : IComponent => WaitForElementsCore(renderedComponent, cssSelector, matchElementCount: null, timeout: timeout); @@ -78,8 +78,8 @@ public static IRefreshableElementCollection WaitForElementsThe exact number of elements to that the should match. /// The maximum time to wait for elements to appear. /// Thrown if no elements is found matching the within the default timeout. - /// The . - public static IRefreshableElementCollection WaitForElements(this IRenderedComponent renderedComponent, string cssSelector, int matchElementCount, TimeSpan timeout) + /// The . + public static IReadOnlyList WaitForElements(this IRenderedComponent renderedComponent, string cssSelector, int matchElementCount, TimeSpan timeout) where TComponent : IComponent => WaitForElementsCore(renderedComponent, cssSelector, matchElementCount: matchElementCount, timeout: timeout); @@ -115,8 +115,8 @@ internal static Task WaitForElementAsync(this IRenderedCom /// The CSS selector to use to search for elements. /// The exact number of elements to that the should match. /// Thrown if no elements is found matching the within the default timeout. - /// The . - internal static Task> WaitForElementsAsync(this IRenderedComponent renderedComponent, string cssSelector, int matchElementCount) + /// The . + internal static Task> WaitForElementsAsync(this IRenderedComponent renderedComponent, string cssSelector, int matchElementCount) where TComponent : IComponent => WaitForElementsCoreAsync(renderedComponent, cssSelector, matchElementCount: matchElementCount, timeout: null); @@ -128,8 +128,8 @@ internal static Task> WaitForElementsAsy /// The CSS selector to use to search for elements. /// The maximum time to wait for elements to appear. /// Thrown if no elements is found matching the within the default timeout. - /// The . - internal static Task> WaitForElementsAsync(this IRenderedComponent renderedComponent, string cssSelector, TimeSpan timeout) + /// The . + internal static Task> WaitForElementsAsync(this IRenderedComponent renderedComponent, string cssSelector, TimeSpan timeout) where TComponent : IComponent => WaitForElementsCoreAsync(renderedComponent, cssSelector, matchElementCount: null, timeout: timeout); @@ -142,8 +142,8 @@ internal static Task> WaitForElementsAsy /// The exact number of elements to that the should match. /// The maximum time to wait for elements to appear. /// Thrown if no elements is found matching the within the default timeout. - /// The . - internal static Task> WaitForElementsAsync(this IRenderedComponent renderedComponent, string cssSelector, int matchElementCount, TimeSpan timeout) + /// The . + internal static Task> WaitForElementsAsync(this IRenderedComponent renderedComponent, string cssSelector, int matchElementCount, TimeSpan timeout) where TComponent : IComponent => WaitForElementsCoreAsync(renderedComponent, cssSelector, matchElementCount: matchElementCount, timeout: timeout); @@ -154,8 +154,8 @@ internal static Task> WaitForElementsAsy /// The render fragment or component find the matching element in. /// The CSS selector to use to search for elements. /// Thrown if no elements is found matching the within the default timeout. - /// The . - internal static Task> WaitForElementsAsync(this IRenderedComponent renderedComponent, string cssSelector) + /// The . + internal static Task> WaitForElementsAsync(this IRenderedComponent renderedComponent, string cssSelector) where TComponent : IComponent => WaitForElementsCoreAsync(renderedComponent, cssSelector, matchElementCount: null, timeout: null); @@ -185,7 +185,7 @@ private static async Task WaitForElementCoreAsync(this IRe return await waiter.WaitTask; } - private static IRefreshableElementCollection WaitForElementsCore( + private static IReadOnlyList WaitForElementsCore( this IRenderedComponent renderedComponent, string cssSelector, int? matchElementCount, @@ -207,7 +207,7 @@ private static IRefreshableElementCollection WaitForElementsCore> WaitForElementsCoreAsync( + private static async Task> WaitForElementsCoreAsync( this IRenderedComponent renderedComponent, string cssSelector, int? matchElementCount, diff --git a/src/bunit/Extensions/WaitForHelpers/WaitForElementsHelper.cs b/src/bunit/Extensions/WaitForHelpers/WaitForElementsHelper.cs index 53f477193..e53ab8dfb 100644 --- a/src/bunit/Extensions/WaitForHelpers/WaitForElementsHelper.cs +++ b/src/bunit/Extensions/WaitForHelpers/WaitForElementsHelper.cs @@ -7,7 +7,7 @@ namespace Bunit.Extensions.WaitForHelpers; /// /// Represents an async wait helper, that will wait for a specified time for element(s) to become available in the DOM. /// -internal class WaitForElementsHelper : WaitForHelper, TComponent> +internal class WaitForElementsHelper : WaitForHelper, TComponent> where TComponent : IComponent { internal const string TimeoutBeforeFoundMessage = "The CSS selector did not result in any matching element(s) before the timeout period passed."; diff --git a/tests/bunit.tests/BlazorE2E/ComponentRenderingTest.cs b/tests/bunit.tests/BlazorE2E/ComponentRenderingTest.cs index f7ec92b68..85dca8d6a 100644 --- a/tests/bunit.tests/BlazorE2E/ComponentRenderingTest.cs +++ b/tests/bunit.tests/BlazorE2E/ComponentRenderingTest.cs @@ -106,15 +106,17 @@ public void CanTriggerKeyPressEvents() // List is initially empty var cut = Render(); var inputElement = cut.Find("input"); - var liElements = cut.FindAll("li", enableAutoRefresh: true); + var liElements = cut.FindAll("li"); liElements.ShouldBeEmpty(); // Typing adds element inputElement.KeyPress("a"); + liElements = cut.FindAll("li"); liElements.ShouldAllBe(li => Assert.Equal("a", li.TextContent)); // Typing again adds another element inputElement.KeyPress("b"); + liElements = cut.FindAll("li"); liElements.ShouldAllBe( li => Assert.Equal("a", li.TextContent), li => Assert.Equal("b", li.TextContent)); @@ -285,7 +287,7 @@ public void CanRenderFragmentsWhilePreservingSurroundingElements() var cut = Render(); var originalButton = cut.Find("button"); - var fragmentElements = cut.FindAll("p[name=fragment-element]", enableAutoRefresh: true); + var fragmentElements = cut.FindAll("p[name=fragment-element]"); Assert.Empty(fragmentElements); // The JS-side DOM builder handles regions correctly, placing elements @@ -294,10 +296,12 @@ public void CanRenderFragmentsWhilePreservingSurroundingElements() // When we click the button, the region is shown originalButton.Click(); + fragmentElements = cut.FindAll("p[name=fragment-element]"); fragmentElements.Single().ShouldNotBeNull(); // The button itself was preserved, so we can click it again and see the effect originalButton.Click(); + fragmentElements = cut.FindAll("p[name=fragment-element]"); Assert.Empty(fragmentElements); } @@ -516,11 +520,12 @@ public void CanRenderMultipleChildContent() e => Assert.Equal("Col2", e.TextContent), e => Assert.Equal("Col3", e.TextContent)); - var tfootElements = cut.FindAll("table tfoot td", enableAutoRefresh: true); + var tfootElements = cut.FindAll("table tfoot td"); Assert.Empty(tfootElements); var toggle = cut.Find("#toggle"); toggle.Change(true); + tfootElements = cut.FindAll("table tfoot td"); Assert.Collection( tfootElements, e => Assert.Equal("The", e.TextContent), diff --git a/tests/bunit.tests/Extensions/RefreshableQueryCollectionTest.cs b/tests/bunit.tests/Extensions/RefreshableQueryCollectionTest.cs deleted file mode 100644 index 91036fb81..000000000 --- a/tests/bunit.tests/Extensions/RefreshableQueryCollectionTest.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace Bunit; - -public class RefreshableQueryCollectionTest : BunitContext -{ - [Fact(DisplayName = "When the query returns no elements, the collection is empty")] - public void Test001() - { - var cut = Render(); - - var sut = new RefreshableElementCollection(cut, ".foo"); - - sut.ShouldBeEmpty(); - } - - [Fact(DisplayName = "When the query returns elements, the collection contains those elements")] - public void Test002() - { - var cut = Render(); - - var sut = new RefreshableElementCollection(cut, "h1"); - - sut.Count.ShouldBe(1); - sut[0].TagName.ShouldBe("H1"); - } - - [Fact(DisplayName = "When Refresh is called, the query is run again and new elements are made available")] - public void Test003() - { - var cut = Render(); - var sut = new RefreshableElementCollection(cut, "li"); - sut.Count.ShouldBe(0); - - cut.Find("button").Click(); - - sut.Refresh(); - sut.Count.ShouldBe(1); - } - - [Fact(DisplayName = "Enabling auto refresh automatically refreshes query when the rendered fragment renders and has changes")] - public void Test004() - { - var cut = Render(); - var sut = new RefreshableElementCollection(cut, "li") { EnableAutoRefresh = true }; - sut.Count.ShouldBe(0); - - cut.Find("button").Click(); - - sut.Count.ShouldBe(1); - } - - [Fact(DisplayName = "Disabling auto refresh turns off automatic refreshing queries on when rendered fragment changes")] - public void Test005() - { - var cut = Render(); - var sut = new RefreshableElementCollection(cut, "li") { EnableAutoRefresh = true }; - - sut.EnableAutoRefresh = false; - - cut.Find("button").Click(); - - sut.Count.ShouldBe(0); - } -}