Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FindByLabelText to find elements by the text of their labels #1252

Merged
merged 42 commits into from Mar 17, 2024

Conversation

scottsauber
Copy link
Contributor

@scottsauber scottsauber commented Nov 1, 2023

Pull request description

Add queries inspired by testing-library but with .NET lean. Fixes #938

PR meta checklist

  • Pull request is targeted at main branch for code
    or targeted at stable branch for documentation that is live on bunit.dev.
  • Pull request is linked to all related issues, if any.
  • I have read the CONTRIBUTING.md document.

Code PR specific checklist

  • My code follows the code style of this project and AspNetCore coding guidelines.
  • My change requires a change to the documentation.
    • I have updated the documentation accordingly.
  • I have updated the appropriate sub section in the CHANGELOG.md.
  • I have added, updated or removed tests to according to my changes.
    • All tests passed.

My own TODOs

  • Rewrite history using Conventional Commits because I didn't read CONTRIBUTING.md before starting this PR and wrongly assumed we could squash. 🤦‍♂️
  • Stop using LabelQueryComponent.razor and convert to using Wrapper and ChildContent for clearer, less disconnected tests
  • Add support for Normalization of whitespace
  • Fix all warnings (ie XML doc comments)

Egil and I chatted about punting for another PR:

@scottsauber scottsauber force-pushed the query-time branch 2 times, most recently from 4717a1b to 120227d Compare November 1, 2023 04:25
@scottsauber
Copy link
Contributor Author

scottsauber commented Nov 1, 2023

Open to any feedback thus far @egil and @linkdotnet before I go further, but I'll keep trucking tomorrow if you don't get to reviewing this sooner, because there are known gaps I need to cover (identified below)

Functionality provided thus far:

  • Find an input/select/textarea/meter/progress/button/output by a label with the for attribute
  • Find an input/select/textarea/meter/progress/button/output by a label wrapped around the tag
  • Find an input/select/textarea/meter/progress/button/output with the aria-label attribute
  • Find an input/select/textarea/meter/progress/button/output by any HTML element with the aria-labelledby attribute

Known gaps thus far:

  • No support for any other RTL-like queries besides Label
  • Needs Overload for ByLabelTextOptions to support things like case sensitivity/insensitivity
  • No support for *OrDefault to support null
  • No support for an *All to find multiple with the same label text (although this feels wrong to have multiple inputs with the same label, but I'm sure there's some edge case I've never run into?)
  • Review byLabelText tests in DTL to see if I'm covering edgecases

Outstanding Questions I have:

  • Do we need to support async versions? None of the APIs used under the hood are async. If so, is the intent of these to allow retries for asynchronous changes to the Blazor component?
  • Add new exception?

@egil
Copy link
Member

egil commented Nov 1, 2023

Excellent Scott, I'll try to find the time later today to take a look.

@egil
Copy link
Member

egil commented Nov 1, 2023

There is a feature of bUnit that unfortunately complicates this quite a bit; the wrappers around returned AngleSharp elements and nodes.

The feature means that if you do a var elm = cut.Find("p") and the <p /> is recreated after a render, the referenced elm type automatically points to the new P element, and not the previous one.

For example, if the classic counter test case was written like this:

  [Fact]
  public void CounterShouldIncrementWhenClicked()
  {
    var cut = Render(@<Counter />);
    var status = cut.Find("p");
    status.MarkupMatches(@<p>Current count: 0</p>);

    cut.Find("button").Click();

    status.MarkupMatches(@<p>Current count: 1</p>);
  }

The test will still pass.

That is because status is a Wrapper<IHtmlParagraphElement> and a IHtmlParagraphElement. When a render completes, the Wrapper<IHtmlParagraphElement> queries the DOM again and finds the new paragraph which it replaces with the previous one.

For all the queries that are based directly on a CSS selector, we can reuse the existing infrastructure, e.g.:

internal class LabelTextUsingAriaLabelStrategy : ILabelTextQueryStrategy
{
  public IElement? FindElement(IRenderedFragment renderedFragment, string labelText)
  {
    var cssSelector= $"[aria-label='{labelText}']";
    var results = renderedFragment.Nodes.QuerySelector(cssSelector);
    
    return results is not null
      ? ElementWrapperFactory.Create(result, renderedFragment, cssSelector)
      : null;
  }
}

For the more complex queries, we need a custom implementation of ElementFactory<TElement> which can re-execute the search after renders.

Perhaps a version which essentially calls FindByLabelText, something like this, perhaps:

// bUnit\src\bunit.web.query\Labels\LabelQueryExtensions.cs
using AngleSharp.Dom;

namespace Bunit;

public static class LabelQueryExtensions
{
	private static readonly List<ILabelTextQueryStrategy> LabelTextQueryStrategies = new()
	{
		// This is intentionally in the order of most likely to minimize strategies tried to find the label
		new LabelTextUsingForAttributeStrategy(),
		new LabelTextUsingAriaLabelStrategy(),
		new LabelTextUsingWrappedElementStrategy(),
		new LabelTextUsingAriaLabelledByStrategy(),
	};

	/// <summary>
	/// Returns the first element (i.e. an input, select, textarea, etc. element) associated with the given label text.
	/// </summary>
	/// <param name="renderedFragment">The rendered fragment to search.</param>
	/// <param name="labelText">The text of the label to search (i.e. the InnerText of the Label, such as "First Name" for a `<label>First Name</label>`)</param>
	public static IElement FindByLabelText(this IRenderedFragment renderedFragment, string labelText)
	{
		return FindByLabelTextInternal(renderedFragment, labelText) ?? throw new ElementNotFoundException(labelText);
	}

	/// <summary>
	/// Returns the first element (i.e. an input, select, textarea, etc. element) associated with the given label text.
	/// </summary>
	/// <param name="renderedFragment">The rendered fragment to search.</param>
	/// <param name="labelText">The text of the label to search (i.e. the InnerText of the Label, such as "First Name" for a `<label>First Name</label>`)</param>
	internal static IElement? FindByLabelTextInternal(this IRenderedFragment renderedFragment, string labelText)
	{
		try
		{
			foreach (var strategy in LabelTextQueryStrategies)
			{
				var element = strategy.FindElement(renderedFragment, labelText);

				if (element != null)
					return element;
			}
		}
		catch (DomException exception) when (exception.Message == "The string did not match the expected pattern.")
		{
			return null;
		}

		return null;
	}
}
// bUnit\src\bunit.web.query\ElementWrapperFactory.cs
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Svg.Dom;
using AngleSharpWrappers;

namespace Bunit;

internal static class ElementWrapperFactory
{
	public static IElement CreateByLabelText(IElement element, IRenderedFragment renderedFragment, string labelText)
	{
		return element switch
		{
			IHtmlAnchorElement htmlAnchorElement => new HtmlAnchorElementWrapper(
				new ByLabelTextElementFactory<IHtmlAnchorElement>(renderedFragment, htmlAnchorElement, labelText)),
			IHtmlAreaElement htmlAreaElement => new HtmlAreaElementWrapper(
				new ByLabelTextElementFactory<IHtmlAreaElement>(renderedFragment, htmlAreaElement, labelText)),
			IHtmlAudioElement htmlAudioElement => new HtmlAudioElementWrapper(
				new ByLabelTextElementFactory<IHtmlAudioElement>(renderedFragment, htmlAudioElement, labelText)),
			IHtmlBaseElement htmlBaseElement => new HtmlBaseElementWrapper(
				new ByLabelTextElementFactory<IHtmlBaseElement>(renderedFragment, htmlBaseElement, labelText)),
			IHtmlBodyElement htmlBodyElement => new HtmlBodyElementWrapper(
				new ByLabelTextElementFactory<IHtmlBodyElement>(renderedFragment, htmlBodyElement, labelText)),
			IHtmlBreakRowElement htmlBreakRowElement => new HtmlBreakRowElementWrapper(
				new ByLabelTextElementFactory<IHtmlBreakRowElement>(renderedFragment, htmlBreakRowElement, labelText)),
			IHtmlButtonElement htmlButtonElement => new HtmlButtonElementWrapper(
				new ByLabelTextElementFactory<IHtmlButtonElement>(renderedFragment, htmlButtonElement, labelText)),
			IHtmlCanvasElement htmlCanvasElement => new HtmlCanvasElementWrapper(
				new ByLabelTextElementFactory<IHtmlCanvasElement>(renderedFragment, htmlCanvasElement, labelText)),
			IHtmlCommandElement htmlCommandElement => new HtmlCommandElementWrapper(
				new ByLabelTextElementFactory<IHtmlCommandElement>(renderedFragment, htmlCommandElement, labelText)),
			IHtmlDataElement htmlDataElement => new HtmlDataElementWrapper(
				new ByLabelTextElementFactory<IHtmlDataElement>(renderedFragment, htmlDataElement, labelText)),
			IHtmlDataListElement htmlDataListElement => new HtmlDataListElementWrapper(
				new ByLabelTextElementFactory<IHtmlDataListElement>(renderedFragment, htmlDataListElement, labelText)),
			IHtmlDetailsElement htmlDetailsElement => new HtmlDetailsElementWrapper(
				new ByLabelTextElementFactory<IHtmlDetailsElement>(renderedFragment, htmlDetailsElement, labelText)),
			IHtmlDialogElement htmlDialogElement => new HtmlDialogElementWrapper(
				new ByLabelTextElementFactory<IHtmlDialogElement>(renderedFragment, htmlDialogElement, labelText)),
			IHtmlDivElement htmlDivElement => new HtmlDivElementWrapper(
				new ByLabelTextElementFactory<IHtmlDivElement>(renderedFragment, htmlDivElement, labelText)),
			IHtmlEmbedElement htmlEmbedElement => new HtmlEmbedElementWrapper(
				new ByLabelTextElementFactory<IHtmlEmbedElement>(renderedFragment, htmlEmbedElement, labelText)),
			IHtmlFieldSetElement htmlFieldSetElement => new HtmlFieldSetElementWrapper(
				new ByLabelTextElementFactory<IHtmlFieldSetElement>(renderedFragment, htmlFieldSetElement, labelText)),
			IHtmlFormElement htmlFormElement => new HtmlFormElementWrapper(
				new ByLabelTextElementFactory<IHtmlFormElement>(renderedFragment, htmlFormElement, labelText)),
			IHtmlHeadElement htmlHeadElement => new HtmlHeadElementWrapper(
				new ByLabelTextElementFactory<IHtmlHeadElement>(renderedFragment, htmlHeadElement, labelText)),
			IHtmlHeadingElement htmlHeadingElement => new HtmlHeadingElementWrapper(
				new ByLabelTextElementFactory<IHtmlHeadingElement>(renderedFragment, htmlHeadingElement, labelText)),
			IHtmlHrElement htmlHrElement => new HtmlHrElementWrapper(
				new ByLabelTextElementFactory<IHtmlHrElement>(renderedFragment, htmlHrElement, labelText)),
			IHtmlHtmlElement htmlHtmlElement => new HtmlHtmlElementWrapper(
				new ByLabelTextElementFactory<IHtmlHtmlElement>(renderedFragment, htmlHtmlElement, labelText)),
			IHtmlImageElement htmlImageElement => new HtmlImageElementWrapper(
				new ByLabelTextElementFactory<IHtmlImageElement>(renderedFragment, htmlImageElement, labelText)),
			IHtmlInlineFrameElement htmlInlineFrameElement => new HtmlInlineFrameElementWrapper(
				new ByLabelTextElementFactory<IHtmlInlineFrameElement>(renderedFragment, htmlInlineFrameElement, labelText)),
			IHtmlInputElement htmlInputElement => new HtmlInputElementWrapper(
				new ByLabelTextElementFactory<IHtmlInputElement>(renderedFragment, htmlInputElement, labelText)),
			IHtmlKeygenElement htmlKeygenElement => new HtmlKeygenElementWrapper(
				new ByLabelTextElementFactory<IHtmlKeygenElement>(renderedFragment, htmlKeygenElement, labelText)),
			IHtmlLabelElement htmlLabelElement => new HtmlLabelElementWrapper(
				new ByLabelTextElementFactory<IHtmlLabelElement>(renderedFragment, htmlLabelElement, labelText)),
			IHtmlLegendElement htmlLegendElement => new HtmlLegendElementWrapper(
				new ByLabelTextElementFactory<IHtmlLegendElement>(renderedFragment, htmlLegendElement, labelText)),
			IHtmlLinkElement htmlLinkElement => new HtmlLinkElementWrapper(
				new ByLabelTextElementFactory<IHtmlLinkElement>(renderedFragment, htmlLinkElement, labelText)),
			IHtmlListItemElement htmlListItemElement => new HtmlListItemElementWrapper(
				new ByLabelTextElementFactory<IHtmlListItemElement>(renderedFragment, htmlListItemElement, labelText)),
			IHtmlMapElement htmlMapElement => new HtmlMapElementWrapper(
				new ByLabelTextElementFactory<IHtmlMapElement>(renderedFragment, htmlMapElement, labelText)),
			IHtmlMarqueeElement htmlMarqueeElement => new HtmlMarqueeElementWrapper(
				new ByLabelTextElementFactory<IHtmlMarqueeElement>(renderedFragment, htmlMarqueeElement, labelText)),
			IHtmlMenuElement htmlMenuElement => new HtmlMenuElementWrapper(
				new ByLabelTextElementFactory<IHtmlMenuElement>(renderedFragment, htmlMenuElement, labelText)),
			IHtmlMenuItemElement htmlMenuItemElement => new HtmlMenuItemElementWrapper(
				new ByLabelTextElementFactory<IHtmlMenuItemElement>(renderedFragment, htmlMenuItemElement, labelText)),
			IHtmlMetaElement htmlMetaElement => new HtmlMetaElementWrapper(
				new ByLabelTextElementFactory<IHtmlMetaElement>(renderedFragment, htmlMetaElement, labelText)),
			IHtmlMeterElement htmlMeterElement => new HtmlMeterElementWrapper(
				new ByLabelTextElementFactory<IHtmlMeterElement>(renderedFragment, htmlMeterElement, labelText)),
			IHtmlModElement htmlModElement => new HtmlModElementWrapper(
				new ByLabelTextElementFactory<IHtmlModElement>(renderedFragment, htmlModElement, labelText)),
			IHtmlObjectElement htmlObjectElement => new HtmlObjectElementWrapper(
				new ByLabelTextElementFactory<IHtmlObjectElement>(renderedFragment, htmlObjectElement, labelText)),
			IHtmlOrderedListElement htmlOrderedListElement => new HtmlOrderedListElementWrapper(
				new ByLabelTextElementFactory<IHtmlOrderedListElement>(renderedFragment, htmlOrderedListElement, labelText)),
			IHtmlParagraphElement htmlParagraphElement => new HtmlParagraphElementWrapper(
				new ByLabelTextElementFactory<IHtmlParagraphElement>(renderedFragment, htmlParagraphElement, labelText)),
			IHtmlParamElement htmlParamElement => new HtmlParamElementWrapper(
				new ByLabelTextElementFactory<IHtmlParamElement>(renderedFragment, htmlParamElement, labelText)),
			IHtmlPreElement htmlPreElement => new HtmlPreElementWrapper(
				new ByLabelTextElementFactory<IHtmlPreElement>(renderedFragment, htmlPreElement, labelText)),
			IHtmlProgressElement htmlProgressElement => new HtmlProgressElementWrapper(
				new ByLabelTextElementFactory<IHtmlProgressElement>(renderedFragment, htmlProgressElement, labelText)),
			IHtmlQuoteElement htmlQuoteElement => new HtmlQuoteElementWrapper(
				new ByLabelTextElementFactory<IHtmlQuoteElement>(renderedFragment, htmlQuoteElement, labelText)),
			IHtmlScriptElement htmlScriptElement => new HtmlScriptElementWrapper(
				new ByLabelTextElementFactory<IHtmlScriptElement>(renderedFragment, htmlScriptElement, labelText)),
			IHtmlSelectElement htmlSelectElement => new HtmlSelectElementWrapper(
				new ByLabelTextElementFactory<IHtmlSelectElement>(renderedFragment, htmlSelectElement, labelText)),
			IHtmlSourceElement htmlSourceElement => new HtmlSourceElementWrapper(
				new ByLabelTextElementFactory<IHtmlSourceElement>(renderedFragment, htmlSourceElement, labelText)),
			IHtmlSpanElement htmlSpanElement => new HtmlSpanElementWrapper(
				new ByLabelTextElementFactory<IHtmlSpanElement>(renderedFragment, htmlSpanElement, labelText)),
			IHtmlStyleElement htmlStyleElement => new HtmlStyleElementWrapper(
				new ByLabelTextElementFactory<IHtmlStyleElement>(renderedFragment, htmlStyleElement, labelText)),
			IHtmlTableCaptionElement htmlTableCaptionElement => new HtmlTableCaptionElementWrapper(
				new ByLabelTextElementFactory<IHtmlTableCaptionElement>(renderedFragment, htmlTableCaptionElement, labelText)),
			IHtmlTableCellElement htmlTableCellElement => new HtmlTableCellElementWrapper(
				new ByLabelTextElementFactory<IHtmlTableCellElement>(renderedFragment, htmlTableCellElement, labelText)),
			IHtmlTableElement htmlTableElement => new HtmlTableElementWrapper(
				new ByLabelTextElementFactory<IHtmlTableElement>(renderedFragment, htmlTableElement, labelText)),
			IHtmlTableRowElement htmlTableRowElement => new HtmlTableRowElementWrapper(
				new ByLabelTextElementFactory<IHtmlTableRowElement>(renderedFragment, htmlTableRowElement, labelText)),
			IHtmlTableSectionElement htmlTableSectionElement => new HtmlTableSectionElementWrapper(
				new ByLabelTextElementFactory<IHtmlTableSectionElement>(renderedFragment, htmlTableSectionElement, labelText)),
			IHtmlTemplateElement htmlTemplateElement => new HtmlTemplateElementWrapper(
				new ByLabelTextElementFactory<IHtmlTemplateElement>(renderedFragment, htmlTemplateElement, labelText)),
			IHtmlTextAreaElement htmlTextAreaElement => new HtmlTextAreaElementWrapper(
				new ByLabelTextElementFactory<IHtmlTextAreaElement>(renderedFragment, htmlTextAreaElement, labelText)),
			IHtmlTimeElement htmlTimeElement => new HtmlTimeElementWrapper(
				new ByLabelTextElementFactory<IHtmlTimeElement>(renderedFragment, htmlTimeElement, labelText)),
			IHtmlTitleElement htmlTitleElement => new HtmlTitleElementWrapper(
				new ByLabelTextElementFactory<IHtmlTitleElement>(renderedFragment, htmlTitleElement, labelText)),
			IHtmlTrackElement htmlTrackElement => new HtmlTrackElementWrapper(
				new ByLabelTextElementFactory<IHtmlTrackElement>(renderedFragment, htmlTrackElement, labelText)),
			IHtmlUnknownElement htmlUnknownElement => new HtmlUnknownElementWrapper(
				new ByLabelTextElementFactory<IHtmlUnknownElement>(renderedFragment, htmlUnknownElement, labelText)),
			IHtmlVideoElement htmlVideoElement => new HtmlVideoElementWrapper(
				new ByLabelTextElementFactory<IHtmlVideoElement>(renderedFragment, htmlVideoElement, labelText)),
			IHtmlMediaElement htmlMediaElement => new HtmlMediaElementWrapper(
				new ByLabelTextElementFactory<IHtmlMediaElement>(renderedFragment, htmlMediaElement, labelText)),
			IPseudoElement pseudoElement => new PseudoElementWrapper(
				new ByLabelTextElementFactory<IPseudoElement>(renderedFragment, pseudoElement, labelText)),
			ISvgCircleElement svgCircleElement => new SvgCircleElementWrapper(
				new ByLabelTextElementFactory<ISvgCircleElement>(renderedFragment, svgCircleElement, labelText)),
			ISvgDescriptionElement svgDescriptionElement => new SvgDescriptionElementWrapper(
				new ByLabelTextElementFactory<ISvgDescriptionElement>(renderedFragment, svgDescriptionElement, labelText)),
			ISvgForeignObjectElement svgForeignObjectElement => new SvgForeignObjectElementWrapper(
				new ByLabelTextElementFactory<ISvgForeignObjectElement>(renderedFragment, svgForeignObjectElement, labelText)),
			ISvgSvgElement svgSvgElement => new SvgSvgElementWrapper(
				new ByLabelTextElementFactory<ISvgSvgElement>(renderedFragment, svgSvgElement, labelText)),
			ISvgTitleElement svgTitleElement => new SvgTitleElementWrapper(
				new ByLabelTextElementFactory<ISvgTitleElement>(renderedFragment, svgTitleElement, labelText)),
			ISvgElement svgElement => new SvgElementWrapper(
				new ByLabelTextElementFactory<ISvgElement>(renderedFragment, svgElement, labelText)),
			IHtmlElement htmlElement => new HtmlElementWrapper(
				new ByLabelTextElementFactory<IHtmlElement>(renderedFragment, htmlElement, labelText)),
			_ => new ElementWrapper(
				new ByLabelTextElementFactory<IElement>(renderedFragment, element, labelText)),
		};
	}
}

and finally

// bUnit\src\bunit.web.query\Labels\ByLabelTextElementFactory.cs
using AngleSharp.Dom;

namespace Bunit;

using AngleSharpWrappers;

internal sealed class ByLabelTextElementFactory<TElement> : IElementFactory<TElement>
	where TElement : class, IElement
{
	private readonly IRenderedFragment testTarget;
	private readonly string labelText;
	private TElement? element;

	public ByLabelTextElementFactory(IRenderedFragment testTarget, TElement initialElement, string labelText)
	{
		this.testTarget = testTarget;
		element = initialElement;
		this.labelText = labelText;
		testTarget.OnMarkupUpdated += FragmentsMarkupUpdated;
	}

	private void FragmentsMarkupUpdated(object? sender, EventArgs args) => element = null;

	TElement IElementFactory<TElement>.GetElement()
	{
		if (element is null)
		{
			var queryResult = testTarget.FindByLabelTextInternal(labelText);
			element = queryResult as TElement;
		}

		return element ?? throw new ElementRemovedFromDomException(labelText);
	}
}

NOTE: the above code is not tested, just hacked together for illustrative purposes.

In general, probably don't leverage Find and FIndAll, since they returns an wrapped IElement.

Instead, use either renderedFragment.Nodes.QuerySelector or renderedFragment.Nodes.QuerySelectorAll, and only wrap the resulting element(s) when you are ready to return it to the user, using a custom factory that knows how to redo the query after a render.

@scottsauber
Copy link
Contributor Author

Picking this back up tonight/next few days just FYI

@scottsauber scottsauber force-pushed the query-time branch 2 times, most recently from 2a282ca to 5bbb5f2 Compare November 13, 2023 04:50
@scottsauber
Copy link
Contributor Author

There is a feature of bUnit that unfortunately complicates this quite a bit; the wrappers around returned AngleSharp elements and nodes.

The feature means that if you do a var elm = cut.Find("p") and the <p /> is recreated after a render, the referenced elm type automatically points to the new P element, and not the previous one.

For example, if the classic counter test case was written like this:

  [Fact]
  public void CounterShouldIncrementWhenClicked()
  {
    var cut = Render(@<Counter />);
    var status = cut.Find("p");
    status.MarkupMatches(@<p>Current count: 0</p>);

    cut.Find("button").Click();

    status.MarkupMatches(@<p>Current count: 1</p>);
  }

The test will still pass.

That is because status is a Wrapper<IHtmlParagraphElement> and a IHtmlParagraphElement. When a render completes, the Wrapper<IHtmlParagraphElement> queries the DOM again and finds the new paragraph which it replaces with the previous one.

For all the queries that are based directly on a CSS selector, we can reuse the existing infrastructure, e.g.:

internal class LabelTextUsingAriaLabelStrategy : ILabelTextQueryStrategy
{
  public IElement? FindElement(IRenderedFragment renderedFragment, string labelText)
  {
    var cssSelector= $"[aria-label='{labelText}']";
    var results = renderedFragment.Nodes.QuerySelector(cssSelector);
    
    return results is not null
      ? ElementWrapperFactory.Create(result, renderedFragment, cssSelector)
      : null;
  }
}

For the more complex queries, we need a custom implementation of ElementFactory<TElement> which can re-execute the search after renders.

Perhaps a version which essentially calls FindByLabelText, something like this, perhaps:

// bUnit\src\bunit.web.query\Labels\LabelQueryExtensions.cs
using AngleSharp.Dom;

namespace Bunit;

public static class LabelQueryExtensions
{
	private static readonly List<ILabelTextQueryStrategy> LabelTextQueryStrategies = new()
	{
		// This is intentionally in the order of most likely to minimize strategies tried to find the label
		new LabelTextUsingForAttributeStrategy(),
		new LabelTextUsingAriaLabelStrategy(),
		new LabelTextUsingWrappedElementStrategy(),
		new LabelTextUsingAriaLabelledByStrategy(),
	};

	/// <summary>
	/// Returns the first element (i.e. an input, select, textarea, etc. element) associated with the given label text.
	/// </summary>
	/// <param name="renderedFragment">The rendered fragment to search.</param>
	/// <param name="labelText">The text of the label to search (i.e. the InnerText of the Label, such as "First Name" for a `<label>First Name</label>`)</param>
	public static IElement FindByLabelText(this IRenderedFragment renderedFragment, string labelText)
	{
		return FindByLabelTextInternal(renderedFragment, labelText) ?? throw new ElementNotFoundException(labelText);
	}

	/// <summary>
	/// Returns the first element (i.e. an input, select, textarea, etc. element) associated with the given label text.
	/// </summary>
	/// <param name="renderedFragment">The rendered fragment to search.</param>
	/// <param name="labelText">The text of the label to search (i.e. the InnerText of the Label, such as "First Name" for a `<label>First Name</label>`)</param>
	internal static IElement? FindByLabelTextInternal(this IRenderedFragment renderedFragment, string labelText)
	{
		try
		{
			foreach (var strategy in LabelTextQueryStrategies)
			{
				var element = strategy.FindElement(renderedFragment, labelText);

				if (element != null)
					return element;
			}
		}
		catch (DomException exception) when (exception.Message == "The string did not match the expected pattern.")
		{
			return null;
		}

		return null;
	}
}
// bUnit\src\bunit.web.query\ElementWrapperFactory.cs
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Svg.Dom;
using AngleSharpWrappers;

namespace Bunit;

internal static class ElementWrapperFactory
{
	public static IElement CreateByLabelText(IElement element, IRenderedFragment renderedFragment, string labelText)
	{
		return element switch
		{
			IHtmlAnchorElement htmlAnchorElement => new HtmlAnchorElementWrapper(
				new ByLabelTextElementFactory<IHtmlAnchorElement>(renderedFragment, htmlAnchorElement, labelText)),
			IHtmlAreaElement htmlAreaElement => new HtmlAreaElementWrapper(
				new ByLabelTextElementFactory<IHtmlAreaElement>(renderedFragment, htmlAreaElement, labelText)),
			IHtmlAudioElement htmlAudioElement => new HtmlAudioElementWrapper(
				new ByLabelTextElementFactory<IHtmlAudioElement>(renderedFragment, htmlAudioElement, labelText)),
			IHtmlBaseElement htmlBaseElement => new HtmlBaseElementWrapper(
				new ByLabelTextElementFactory<IHtmlBaseElement>(renderedFragment, htmlBaseElement, labelText)),
			IHtmlBodyElement htmlBodyElement => new HtmlBodyElementWrapper(
				new ByLabelTextElementFactory<IHtmlBodyElement>(renderedFragment, htmlBodyElement, labelText)),
			IHtmlBreakRowElement htmlBreakRowElement => new HtmlBreakRowElementWrapper(
				new ByLabelTextElementFactory<IHtmlBreakRowElement>(renderedFragment, htmlBreakRowElement, labelText)),
			IHtmlButtonElement htmlButtonElement => new HtmlButtonElementWrapper(
				new ByLabelTextElementFactory<IHtmlButtonElement>(renderedFragment, htmlButtonElement, labelText)),
			IHtmlCanvasElement htmlCanvasElement => new HtmlCanvasElementWrapper(
				new ByLabelTextElementFactory<IHtmlCanvasElement>(renderedFragment, htmlCanvasElement, labelText)),
			IHtmlCommandElement htmlCommandElement => new HtmlCommandElementWrapper(
				new ByLabelTextElementFactory<IHtmlCommandElement>(renderedFragment, htmlCommandElement, labelText)),
			IHtmlDataElement htmlDataElement => new HtmlDataElementWrapper(
				new ByLabelTextElementFactory<IHtmlDataElement>(renderedFragment, htmlDataElement, labelText)),
			IHtmlDataListElement htmlDataListElement => new HtmlDataListElementWrapper(
				new ByLabelTextElementFactory<IHtmlDataListElement>(renderedFragment, htmlDataListElement, labelText)),
			IHtmlDetailsElement htmlDetailsElement => new HtmlDetailsElementWrapper(
				new ByLabelTextElementFactory<IHtmlDetailsElement>(renderedFragment, htmlDetailsElement, labelText)),
			IHtmlDialogElement htmlDialogElement => new HtmlDialogElementWrapper(
				new ByLabelTextElementFactory<IHtmlDialogElement>(renderedFragment, htmlDialogElement, labelText)),
			IHtmlDivElement htmlDivElement => new HtmlDivElementWrapper(
				new ByLabelTextElementFactory<IHtmlDivElement>(renderedFragment, htmlDivElement, labelText)),
			IHtmlEmbedElement htmlEmbedElement => new HtmlEmbedElementWrapper(
				new ByLabelTextElementFactory<IHtmlEmbedElement>(renderedFragment, htmlEmbedElement, labelText)),
			IHtmlFieldSetElement htmlFieldSetElement => new HtmlFieldSetElementWrapper(
				new ByLabelTextElementFactory<IHtmlFieldSetElement>(renderedFragment, htmlFieldSetElement, labelText)),
			IHtmlFormElement htmlFormElement => new HtmlFormElementWrapper(
				new ByLabelTextElementFactory<IHtmlFormElement>(renderedFragment, htmlFormElement, labelText)),
			IHtmlHeadElement htmlHeadElement => new HtmlHeadElementWrapper(
				new ByLabelTextElementFactory<IHtmlHeadElement>(renderedFragment, htmlHeadElement, labelText)),
			IHtmlHeadingElement htmlHeadingElement => new HtmlHeadingElementWrapper(
				new ByLabelTextElementFactory<IHtmlHeadingElement>(renderedFragment, htmlHeadingElement, labelText)),
			IHtmlHrElement htmlHrElement => new HtmlHrElementWrapper(
				new ByLabelTextElementFactory<IHtmlHrElement>(renderedFragment, htmlHrElement, labelText)),
			IHtmlHtmlElement htmlHtmlElement => new HtmlHtmlElementWrapper(
				new ByLabelTextElementFactory<IHtmlHtmlElement>(renderedFragment, htmlHtmlElement, labelText)),
			IHtmlImageElement htmlImageElement => new HtmlImageElementWrapper(
				new ByLabelTextElementFactory<IHtmlImageElement>(renderedFragment, htmlImageElement, labelText)),
			IHtmlInlineFrameElement htmlInlineFrameElement => new HtmlInlineFrameElementWrapper(
				new ByLabelTextElementFactory<IHtmlInlineFrameElement>(renderedFragment, htmlInlineFrameElement, labelText)),
			IHtmlInputElement htmlInputElement => new HtmlInputElementWrapper(
				new ByLabelTextElementFactory<IHtmlInputElement>(renderedFragment, htmlInputElement, labelText)),
			IHtmlKeygenElement htmlKeygenElement => new HtmlKeygenElementWrapper(
				new ByLabelTextElementFactory<IHtmlKeygenElement>(renderedFragment, htmlKeygenElement, labelText)),
			IHtmlLabelElement htmlLabelElement => new HtmlLabelElementWrapper(
				new ByLabelTextElementFactory<IHtmlLabelElement>(renderedFragment, htmlLabelElement, labelText)),
			IHtmlLegendElement htmlLegendElement => new HtmlLegendElementWrapper(
				new ByLabelTextElementFactory<IHtmlLegendElement>(renderedFragment, htmlLegendElement, labelText)),
			IHtmlLinkElement htmlLinkElement => new HtmlLinkElementWrapper(
				new ByLabelTextElementFactory<IHtmlLinkElement>(renderedFragment, htmlLinkElement, labelText)),
			IHtmlListItemElement htmlListItemElement => new HtmlListItemElementWrapper(
				new ByLabelTextElementFactory<IHtmlListItemElement>(renderedFragment, htmlListItemElement, labelText)),
			IHtmlMapElement htmlMapElement => new HtmlMapElementWrapper(
				new ByLabelTextElementFactory<IHtmlMapElement>(renderedFragment, htmlMapElement, labelText)),
			IHtmlMarqueeElement htmlMarqueeElement => new HtmlMarqueeElementWrapper(
				new ByLabelTextElementFactory<IHtmlMarqueeElement>(renderedFragment, htmlMarqueeElement, labelText)),
			IHtmlMenuElement htmlMenuElement => new HtmlMenuElementWrapper(
				new ByLabelTextElementFactory<IHtmlMenuElement>(renderedFragment, htmlMenuElement, labelText)),
			IHtmlMenuItemElement htmlMenuItemElement => new HtmlMenuItemElementWrapper(
				new ByLabelTextElementFactory<IHtmlMenuItemElement>(renderedFragment, htmlMenuItemElement, labelText)),
			IHtmlMetaElement htmlMetaElement => new HtmlMetaElementWrapper(
				new ByLabelTextElementFactory<IHtmlMetaElement>(renderedFragment, htmlMetaElement, labelText)),
			IHtmlMeterElement htmlMeterElement => new HtmlMeterElementWrapper(
				new ByLabelTextElementFactory<IHtmlMeterElement>(renderedFragment, htmlMeterElement, labelText)),
			IHtmlModElement htmlModElement => new HtmlModElementWrapper(
				new ByLabelTextElementFactory<IHtmlModElement>(renderedFragment, htmlModElement, labelText)),
			IHtmlObjectElement htmlObjectElement => new HtmlObjectElementWrapper(
				new ByLabelTextElementFactory<IHtmlObjectElement>(renderedFragment, htmlObjectElement, labelText)),
			IHtmlOrderedListElement htmlOrderedListElement => new HtmlOrderedListElementWrapper(
				new ByLabelTextElementFactory<IHtmlOrderedListElement>(renderedFragment, htmlOrderedListElement, labelText)),
			IHtmlParagraphElement htmlParagraphElement => new HtmlParagraphElementWrapper(
				new ByLabelTextElementFactory<IHtmlParagraphElement>(renderedFragment, htmlParagraphElement, labelText)),
			IHtmlParamElement htmlParamElement => new HtmlParamElementWrapper(
				new ByLabelTextElementFactory<IHtmlParamElement>(renderedFragment, htmlParamElement, labelText)),
			IHtmlPreElement htmlPreElement => new HtmlPreElementWrapper(
				new ByLabelTextElementFactory<IHtmlPreElement>(renderedFragment, htmlPreElement, labelText)),
			IHtmlProgressElement htmlProgressElement => new HtmlProgressElementWrapper(
				new ByLabelTextElementFactory<IHtmlProgressElement>(renderedFragment, htmlProgressElement, labelText)),
			IHtmlQuoteElement htmlQuoteElement => new HtmlQuoteElementWrapper(
				new ByLabelTextElementFactory<IHtmlQuoteElement>(renderedFragment, htmlQuoteElement, labelText)),
			IHtmlScriptElement htmlScriptElement => new HtmlScriptElementWrapper(
				new ByLabelTextElementFactory<IHtmlScriptElement>(renderedFragment, htmlScriptElement, labelText)),
			IHtmlSelectElement htmlSelectElement => new HtmlSelectElementWrapper(
				new ByLabelTextElementFactory<IHtmlSelectElement>(renderedFragment, htmlSelectElement, labelText)),
			IHtmlSourceElement htmlSourceElement => new HtmlSourceElementWrapper(
				new ByLabelTextElementFactory<IHtmlSourceElement>(renderedFragment, htmlSourceElement, labelText)),
			IHtmlSpanElement htmlSpanElement => new HtmlSpanElementWrapper(
				new ByLabelTextElementFactory<IHtmlSpanElement>(renderedFragment, htmlSpanElement, labelText)),
			IHtmlStyleElement htmlStyleElement => new HtmlStyleElementWrapper(
				new ByLabelTextElementFactory<IHtmlStyleElement>(renderedFragment, htmlStyleElement, labelText)),
			IHtmlTableCaptionElement htmlTableCaptionElement => new HtmlTableCaptionElementWrapper(
				new ByLabelTextElementFactory<IHtmlTableCaptionElement>(renderedFragment, htmlTableCaptionElement, labelText)),
			IHtmlTableCellElement htmlTableCellElement => new HtmlTableCellElementWrapper(
				new ByLabelTextElementFactory<IHtmlTableCellElement>(renderedFragment, htmlTableCellElement, labelText)),
			IHtmlTableElement htmlTableElement => new HtmlTableElementWrapper(
				new ByLabelTextElementFactory<IHtmlTableElement>(renderedFragment, htmlTableElement, labelText)),
			IHtmlTableRowElement htmlTableRowElement => new HtmlTableRowElementWrapper(
				new ByLabelTextElementFactory<IHtmlTableRowElement>(renderedFragment, htmlTableRowElement, labelText)),
			IHtmlTableSectionElement htmlTableSectionElement => new HtmlTableSectionElementWrapper(
				new ByLabelTextElementFactory<IHtmlTableSectionElement>(renderedFragment, htmlTableSectionElement, labelText)),
			IHtmlTemplateElement htmlTemplateElement => new HtmlTemplateElementWrapper(
				new ByLabelTextElementFactory<IHtmlTemplateElement>(renderedFragment, htmlTemplateElement, labelText)),
			IHtmlTextAreaElement htmlTextAreaElement => new HtmlTextAreaElementWrapper(
				new ByLabelTextElementFactory<IHtmlTextAreaElement>(renderedFragment, htmlTextAreaElement, labelText)),
			IHtmlTimeElement htmlTimeElement => new HtmlTimeElementWrapper(
				new ByLabelTextElementFactory<IHtmlTimeElement>(renderedFragment, htmlTimeElement, labelText)),
			IHtmlTitleElement htmlTitleElement => new HtmlTitleElementWrapper(
				new ByLabelTextElementFactory<IHtmlTitleElement>(renderedFragment, htmlTitleElement, labelText)),
			IHtmlTrackElement htmlTrackElement => new HtmlTrackElementWrapper(
				new ByLabelTextElementFactory<IHtmlTrackElement>(renderedFragment, htmlTrackElement, labelText)),
			IHtmlUnknownElement htmlUnknownElement => new HtmlUnknownElementWrapper(
				new ByLabelTextElementFactory<IHtmlUnknownElement>(renderedFragment, htmlUnknownElement, labelText)),
			IHtmlVideoElement htmlVideoElement => new HtmlVideoElementWrapper(
				new ByLabelTextElementFactory<IHtmlVideoElement>(renderedFragment, htmlVideoElement, labelText)),
			IHtmlMediaElement htmlMediaElement => new HtmlMediaElementWrapper(
				new ByLabelTextElementFactory<IHtmlMediaElement>(renderedFragment, htmlMediaElement, labelText)),
			IPseudoElement pseudoElement => new PseudoElementWrapper(
				new ByLabelTextElementFactory<IPseudoElement>(renderedFragment, pseudoElement, labelText)),
			ISvgCircleElement svgCircleElement => new SvgCircleElementWrapper(
				new ByLabelTextElementFactory<ISvgCircleElement>(renderedFragment, svgCircleElement, labelText)),
			ISvgDescriptionElement svgDescriptionElement => new SvgDescriptionElementWrapper(
				new ByLabelTextElementFactory<ISvgDescriptionElement>(renderedFragment, svgDescriptionElement, labelText)),
			ISvgForeignObjectElement svgForeignObjectElement => new SvgForeignObjectElementWrapper(
				new ByLabelTextElementFactory<ISvgForeignObjectElement>(renderedFragment, svgForeignObjectElement, labelText)),
			ISvgSvgElement svgSvgElement => new SvgSvgElementWrapper(
				new ByLabelTextElementFactory<ISvgSvgElement>(renderedFragment, svgSvgElement, labelText)),
			ISvgTitleElement svgTitleElement => new SvgTitleElementWrapper(
				new ByLabelTextElementFactory<ISvgTitleElement>(renderedFragment, svgTitleElement, labelText)),
			ISvgElement svgElement => new SvgElementWrapper(
				new ByLabelTextElementFactory<ISvgElement>(renderedFragment, svgElement, labelText)),
			IHtmlElement htmlElement => new HtmlElementWrapper(
				new ByLabelTextElementFactory<IHtmlElement>(renderedFragment, htmlElement, labelText)),
			_ => new ElementWrapper(
				new ByLabelTextElementFactory<IElement>(renderedFragment, element, labelText)),
		};
	}
}

and finally

// bUnit\src\bunit.web.query\Labels\ByLabelTextElementFactory.cs
using AngleSharp.Dom;

namespace Bunit;

using AngleSharpWrappers;

internal sealed class ByLabelTextElementFactory<TElement> : IElementFactory<TElement>
	where TElement : class, IElement
{
	private readonly IRenderedFragment testTarget;
	private readonly string labelText;
	private TElement? element;

	public ByLabelTextElementFactory(IRenderedFragment testTarget, TElement initialElement, string labelText)
	{
		this.testTarget = testTarget;
		element = initialElement;
		this.labelText = labelText;
		testTarget.OnMarkupUpdated += FragmentsMarkupUpdated;
	}

	private void FragmentsMarkupUpdated(object? sender, EventArgs args) => element = null;

	TElement IElementFactory<TElement>.GetElement()
	{
		if (element is null)
		{
			var queryResult = testTarget.FindByLabelTextInternal(labelText);
			element = queryResult as TElement;
		}

		return element ?? throw new ElementRemovedFromDomException(labelText);
	}
}

NOTE: the above code is not tested, just hacked together for illustrative purposes.

In general, probably don't leverage Find and FIndAll, since they returns an wrapped IElement.

Instead, use either renderedFragment.Nodes.QuerySelector or renderedFragment.Nodes.QuerySelectorAll, and only wrap the resulting element(s) when you are ready to return it to the user, using a custom factory that knows how to redo the query after a render.

@egil - This is speculatively resolved with 8617b7b, but would certainly like a double check.

I did write tests to prove this was an issue and is now fixed (Test007 in LabelQueryExtensionTests).

@egil
Copy link
Member

egil commented Nov 13, 2023

@egil - This is speculatively resolved with 8617b7b, but would certainly like a double check.

I did write tests to prove this was an issue and is now fixed (Test007 in LabelQueryExtensionTests).

Looks alright to me.

@scottsauber
Copy link
Contributor Author

@egil - what are your thoughts about this PR just focusing on Labels? Then I can do Roles/Alt Text/etc. in follow up PRs?

Just was trying to limit the amount of files changed, but if you don't mind then I can certainly keep proceeding putting everything in this one.

@egil
Copy link
Member

egil commented Nov 14, 2023

@egil - what are your thoughts about this PR just focusing on Labels? Then I can do Roles/Alt Text/etc. in follow up PRs?

Just was trying to limit the amount of files changed, but if you don't mind then I can certainly keep proceeding putting everything in this one.

I think it is fine to break this into multiple PRs. Most / all changes are going into a new project anyway which we will mark as being in preview anyway.

@egil
Copy link
Member

egil commented Nov 23, 2023

Hey @scottsauber, in the recent PR merged to main (linked above) I changed parts of the anglesharp wrapper code so that it is much easier to extend. So some of the changes here is no longer needed.

@scottsauber
Copy link
Contributor Author

Hey @scottsauber, in the recent PR merged to main (linked above) I changed parts of the anglesharp wrapper code so that it is much easier to extend. So some of the changes here is no longer needed.

Cool I’ll take a look and make the adjustments.

@egil
Copy link
Member

egil commented Nov 23, 2023

Essentially you just need to create a IElementFactory, e.g. like src/bunit.web/Extensions/Internal/CssSelectorElementFactory.cs, and when call elm.WrapUsing(new FindByLabelElementFactory(...)). So the giant switch statement is gone 🙂

@scottsauber
Copy link
Contributor Author

scottsauber commented Nov 25, 2023

Essentially you just need to create a IElementFactory, e.g. like src/bunit.web/Extensions/Internal/CssSelectorElementFactory.cs, and when call elm.WrapUsing(new FindByLabelElementFactory(...)). So the giant switch statement is gone 🙂

I keep getting build warnings. Seems like it's confused between the IElementFactory that requires a <TDocument, TElement> from AngleSharp and the one that's being source generated.

Am I doing something dumb?

Error below:

CssSelectorElementFactory.cs(5,51): Error CS0305 : Using the generic type 'IElementFactory<TDocument, TElement>' requires 2 type arguments

@scottsauber
Copy link
Contributor Author

Disregard above... Rider EAP was lying to me. dotnet build works fine.

@egil
Copy link
Member

egil commented Nov 25, 2023

Let me rename it to IElementWrapperFactory so that it doesn't conflict with AngleSharps interface with the same name.

DONE. rebase on main to get the latest bits.

@egil
Copy link
Member

egil commented Nov 25, 2023

btw. saw your presentation over with the Jetbrains guys. Think it was really good and I agree on most points.

Do think MarkupMatches has value, especially if you are building a reusable/shared component (library). There, the markup is not just an implementation detail. E.g. if you are building a bootstrap library, you have to have a specific structure to your markup and use specific classes to make the bootstrap CSS work.

When building apps, we completely agree. Focus on what is observable by the user, not implementation details.

@egil
Copy link
Member

egil commented Mar 15, 2024

This test fails currently, which doesn't in the testing-library:

	[Fact]
	public void Test020()
	{
		var cut = RenderComponent<Wrapper>(ps =>
			ps.AddChildContent($"""
						<label>
						   <span>Test</span>
						   <span>Label</span>
						   <button />
						 </label>
						"""));
		
		var input = cut.FindByLabelText("Test Label");

		input.ShouldNotBeNull();
	}

This is an interesting case. What if there is text inside the button, should that also be matched? I guess we can use TextContent from the label element to get all the text content of the label.

@egil egil marked this pull request as ready for review March 15, 2024 22:27
@linkdotnet
Copy link
Sponsor Collaborator

linkdotnet commented Mar 16, 2024

This is an interesting case. What if there is text inside the button, should that also be matched? I guess we can use TextContent from the label element to get all the text content of the label.

No, this one would not work - see the spec: https://html.spec.whatwg.org/multipage/forms.html#form-associated-element / https://html.spec.whatwg.org/multipage/forms.html#category-label

Basically, a button can not be a label for another button (which the code would allow). Therefore this case doesn't work.

This should work for example

<label>
  <p><span>Test Label</p></span>
</label>

@scottsauber
Copy link
Contributor Author

scottsauber commented Mar 17, 2024

This is an interesting case. What if there is text inside the button, should that also be matched? I guess we can use TextContent from the label element to get all the text content of the label.

No, this one would not work - see the spec: https://html.spec.whatwg.org/multipage/forms.html#form-associated-element / https://html.spec.whatwg.org/multipage/forms.html#category-label

Basically, a button can not be a label for another button (which the code would allow). Therefore this case doesn't work.

This should work for example

<label>
  <p><span>Test Label</p></span>
</label>

Fixed in 3cacae2

Also added a test in 94f6943 to cover a potential regression even though the existing LabelTextUsingForAttributeStrategy already covered this case by pure happenstance.

@scottsauber
Copy link
Contributor Author

scottsauber commented Mar 17, 2024

Alright - I think I resolved or responded to all comments. Looking forward to getting this merged soon!

@linkdotnet
Copy link
Sponsor Collaborator

Alright - I think I resolved or responded to all comments. Looking forward to getting this merged soon!

LGTM - only question open for me is in regards to the documentation. It's fine if we tackle that in a follow-up PR, but it would e a shame not to have documentation around.

@egil
Copy link
Member

egil commented Mar 17, 2024

Alright - I think I resolved or responded to all comments. Looking forward to getting this merged soon!

LGTM - only question open for me is in regards to the documentation. It's fine if we tackle that in a follow-up PR, but it would e a shame not to have documentation around.

I do prefer getting minimum docs done upfront. Still remember not having any docs after one year and spending two months only writing docs :)

We can probably steal some of the philosophy text from testing-library.com and integrate that into a new "look at the DOM/component tree" page, where we cover all the "Find" methods we have, both these new ones and existing ones.

I know @scottsauber is quite busy these days so I can spend an hour throwing a page together and you guys can review it.

As for releasing this. The plan is to release the query library under a preview tag to collect feedback for a month or two, and then remove the preview sticker.

Egil and I chatted about punting for another PR:

  • Add async support
  • Add support for *OrDefault to support null
  • Optional: All *All support

Lets create issues for all three so they can be tackled in separate PRs. The async support is partially there already, users can do something like this: cut.WaitForAssertion(() => cut.FIndByLabel("foo")). That said, we need dedicated async methods.

I wonder if copilot or chatgpt can convert those tests to c# for us?

@linkdotnet
Copy link
Sponsor Collaborator

linkdotnet commented Mar 17, 2024

ToDo:

  • Extend the ci.yml to publish the bunit.web.query package.

PS: I can also do that later, if wished.

Copy link
Member

@egil egil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@egil egil enabled auto-merge March 17, 2024 17:59
@linkdotnet
Copy link
Sponsor Collaborator

Hmm I tried to add a commit - but it just creates a new branch on our repo instead.
I did the classic : git fetch origin pull/1252/head:TestingLibrary and so on.

Anyway - can someone add this line to the ci.yml

        dotnet pack src/bunit.generators/ -c release --output ${{ env.NUGET_DIRECTORY }} -p:ContinuousIntegrationBuild=true -p:publicrelease=true
+       dotnet pack src/bunit.web.query/ -c release --output ${{ env.NUGET_DIRECTORY }} -p:ContinuousIntegrationBuild=true -p:publicrelease=true

Also we should squash the commits.

@linkdotnet
Copy link
Sponsor Collaborator

Also not sure, why we close #938 with this? Maybe we want to use this is as a tracking issue

@egil egil disabled auto-merge March 17, 2024 20:23
@egil egil merged commit c40735b into bUnit-dev:main Mar 17, 2024
10 checks passed
@scottsauber
Copy link
Contributor Author

image

(other than all the other queries and adding async/FirstOrDefault/All support to labels 😅)

@scottsauber
Copy link
Contributor Author

Wonder if we should mention the a11y benefits of FindByLabelText in the docs.

@egil
Copy link
Member

egil commented Mar 18, 2024

Wonder if we should mention the a11y benefits of FindByLabelText in the docs.

Yeah, I just throw something together in my jetlagged haze. Feel free to add more and or a completely new page that explains the philosophy and approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

bUnit version of testing-library.com
3 participants