Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 7, 2026

Playwright UI Test Integration - Complete with Code Review Feedback Addressed

Core Architecture

  • UITestServiceBase: Abstract base class containing all button interaction logic
    • Subclasses only responsible for providing IPage instance
    • Eliminates code duplication between HeadlessUITestService and BlazorWebViewUITestService
  • IBrowserProvider interface: Renamed BrowserPage to TestContext for clarity
    • For Playwright: returns IPage instance
    • For WebView2: returns remote debugging port (int)
  • TestingSettings: Made public with WebViewRemoteDebuggingPort configuration

Headless Host Integration

  • PlaywrightBrowserLifetimeService implements IBrowserProvider (remains internal)
  • Removed unused Page and Browser properties
  • HeadlessUITestService extends UITestServiceBase, provides synchronous IPage access

WebView2 Support (WPF/Console)

  • Bl azorWindowServicesSetter: Combined WebView2 configuration with service provider setup
    • Configures WebView2 with remote debugging when test mode enabled
    • Uses TestingSettings instead of raw IConfiguration
    • Removed unnecessary unique user data folder creation
  • WebView2BrowserProvider: Uses TestingSettings to provide remote debugging port
  • BlazorWebViewUITestService: Extends UITestServiceBase, connects via Playwright CDP

Test Infrastructure

  • UIServiceType enum: Explicit control over which UI service implementation to use
  • AdaptiveRemoteHostSettings: Added UIService property (defaults based on ExePath)
  • AdaptiveRemoteHost: Uses UIService property instead of string matching on ExePath
  • Synchronous wrapper extensions with proper timeout exception handling
  • CreateLazyTestService helper reduces duplication

Tests Passing ✅

  • HeadlessHost_StartsAndRespondsToTestCommands ✅
  • HeadlessHost_UIInteraction_ExitButtonClickable ✅
  • Solution builds with 0 warnings/errors ✅
Original prompt

This section details on the original issue you should resolve

<issue_title>Use Playwright to drive UI in E2E tests</issue_title>
<issue_description>Is your feature request related to a problem? Please describe.
As we add tests for more functionality, it will be useful to drive the tests directly through the HTML-based UI rather than internal services, to test more of the functionality end-to-end.

Describe the solution you'd like
The headless host already uses Playwright to launch its headless browser. I would also like the BlazorWebView to allow Playwright to attach, so the tests can drive the application via the same interface regardless of which host is used.

Describe alternatives you've considered
We're currently driving the test by invoking commands directly, but this is not testing the Blazor view's ability to invoke commands.

Additional context

Playwright driver design — AdaptiveRemote End-to-End Tests

Exit criteria: each host can configure its browser so that Playwright can connect to it and inspect/interact with UI elements. Tests are updated to validate proper display of some UI elements, test execution of a command, and invoke shutdown via the Exit button.

Overview

This document describes a small UI test service and host configuration guidance to allow the existing end-to-end test suite to drive the application's UI using Playwright rather than invoking internal services directly. The aim is to make tests exercise the real HTML/CSS UI produced by the Blazor app so tests better reflect user experience.

Goals

  • Allow E2E tests to query UI state (visibility/enabled) and perform interactions (click) using a minimal intention-based API.
  • Support both the Headless host (already using Playwright) and GUI hosts that use WebView2 via BlazorWebView (WPF and Console).
  • Keep the test API intentionally small: start with IsButtonVisible, IsButtonEnabled, and ClickButton that locate buttons by their exact visible label (trimmed), case-sensitive, and fail if multiple matches exist.
  • Provide concrete guidance for configuring WebView2 so Playwright can attach to the running renderer for GUI hosts.
  • Allow future expansion of the test API, so that more scenarios can be supported easily

Test service: IUITestService (intent-based)

Location and loading

  • Interface: AdaptiveRemote.App alongside ITestEndpoint and IApplicationTestService.
  • Implementation: AdaptiveRemote.EndToEndTests.TestServices and loaded into host processes via ITestEndpoint so implementations have direct access to host Playwright/WebView2 objects.
  • RPC: The interface must be usable across the test-process / host-process boundary through the existing test RPC mechanism (same pattern as IApplicationTestService).

API surface (async)

  • Task<bool> IsButtonVisibleAsync(string label, CancellationToken cancellationToken = default)
  • Task<bool> IsButtonEnabledAsync(string label, CancellationToken cancellationToken = default)
  • Task ClickButtonAsync(string label, CancellationToken cancellationToken = default)

Concrete interface (C#)

using System;
using System.Threading;
using System.Threading.Tasks;
using StreamJsonRpc;

namespace AdaptiveRemote.Services.Testing
{
    [RpcMarshalable]
    public interface IUITestService : IDisposable
    {
        Task<bool> IsButtonVisibleAsync(string label, CancellationToken cancellationToken = default);
        Task<bool> IsButtonEnabledAsync(string label, CancellationToken cancellationToken = default);
        Task ClickButtonAsync(string label, CancellationToken cancellationToken = default);
    }
}

Matching rules

  • Match by the element's visible text only; no selectors or automation ids are accepted.
  • Exact match after trimming surrounding whitespace.
  • Case-sensitive.
  • If multiple elements match the label, methods must fail with an error indicating ambiguous matches (tests must disambiguate).

Behavior details

  • IsButtonVisibleAsync returns true only if a matching element exists in the rendered DOM and is visible (i.e., would be perceivable to a user). Use Playwright's IsVisible semantics for implementation (hidden elements, display:none, aria-hidden, opacity:0 treated as not visible).
  • IsButtonEnabledAsync returns true only if the element has no disabled attribute and is not inert via aria-disabled="true" (treat aria-disabled as disabled for our tests).
  • ClickButtonAsync must ensure the element is visible and enabled before performing a click; otherwise it should throw a clear exception. After issuing the click it should wait for Playwright's default navigation/async stability mechanisms (e.g., wait for network/DOM settles) for a short timeout (default 2s) to let UI updates happen, but callers must still perform explicit assertions where needed.

Synchronous wrappers (test-side)

  • Provide extension methods (in test code) that wrap the async APIs using ...

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Implement Playwright for driving UI in E2E tests Add Playwright UI test service for E2E testing Jan 7, 2026
Copilot AI requested a review from jodavis January 7, 2026 17:50
@jodavis
Copy link
Owner

jodavis commented Jan 7, 2026

@copilot Add WebView2 support for the WPF/Console hosts as well. You won't be able to run them because they require Windows, but there needs to be an abstraction defined that will make both hosts work. It looks like the opportunity would be to have AdaptiveRemoteHost choose HeadlessUITestService or BlazorWebViewUITestService when calling CreateUITestServiceAsync, depending on the type of host. You should also add the support for passing the command line parameters/environment to the BlazorWebView.

Other notes:
src/AdaptiveRemote.Headless/PlaywrightBrowserLifetimeService.cs:11 - Can we create an interface for this, instead of making the whole class public?

test/AdaptiveRemote.EndtoEndTests.TestServices/AdaptiveRemote.EndtoEndTests.TestServices.csproj:19 - If we add a shared test interface, then the TestServices project won't have to take a direct dependency on this specific host project.

test/AdaptiveRemote.EndtoEndTests.TestServices/Host/AdaptiveRemoteHost.cs:53 - Consider a shared helper method for the Lazy constructors, to reduce duplicated code as we add services.

test/AdaptiveRemote.EndtoEndTests.TestServices/HeadlessUITestService.cs:18 - This should hold onto the browserService and access browserService.Page, rather than capturing the IPage right away. The page could change if the browser needs to refresh or navigate elsewhere.

test/AdaptiveRemote.EndtoEndTests.TestServices/HeadlessUITestService.cs:86 - All this logic should be unnecessary. Playwright has the ability to throw meaningful errors if there are no matches or ambiguous matches. We should use that rather than building our own.

test/AdaptiveRemote.EndtoEndTests.TestServices/IUITestServiceExtensions.cs:36 - This should throw a TimeoutException if WaitForAsyncTask returns false. (Maybe WaitForAsyncTask should always throw TimeoutException, except then the exception message can't tell you what timed out.)

test/AdaptiveRemote.EndtoEndTests/HeadlessHostTests.cs:59 - Creating a separate test is ok for now, but I really want to modify RunStandardE2ETestAsync to use the IUITestService instead of InvokeCommand, once all the hosts support IUITestService. This separate test is duplicating a lot of unclear test logic.

Copy link
Contributor Author

Copilot AI commented Jan 7, 2026

@copilot Add WebView2 support for the WPF/Console hosts as well. You won't be able to run them because they require Windows, but there needs to be an abstraction defined that will make both hosts work...

Addressed in commit 684a2ec:

Interface abstraction (line 11): Created IBrowserProvider interface so the class can remain internal.

Shared test interface (line 19): Removed TestServices dependency on Headless project by using IBrowserProvider.

Shared helper method (line 53): Added CreateLazyTestService helper to reduce duplication.

Access Page dynamically (line 18): Updated to hold IBrowserProvider and access BrowserPage property instead of capturing Page at construction.

Simplified locator logic (line 86): Now uses Playwright's GetByRole which throws meaningful errors for missing/ambiguous matches.

TimeoutException (line 36): Fixed ClickButton to throw TimeoutException when WaitForAsyncTask returns false.

Working on WebView2 support and command-line parameters next.

Copilot AI changed the title Add Playwright UI test service for E2E testing Add Playwright UI test service for E2E testing with WebView2 support Jan 7, 2026
Copy link
Owner

Choose a reason for hiding this comment

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

Everything in this file, except the acquisition of the IPage, will want to be shared between the HeadlessHost implementation and the BlazorWebView implementation. (I.e. all the logic regarding how buttons are found, checked, and clicked)

Copy link
Owner

Choose a reason for hiding this comment

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

Maybe there could be a base class with an abstract "IPage" property, so that all these methods work against "IPage" but the subclasses are responsible for how the IPage is acquired.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Created UITestServiceBase abstract class with abstract GetPageAsync() method in commit 11e9d21. All shared logic now in base class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Created UITestServiceBase abstract class in commit 11e9d21. All button interaction logic (finding, checking visibility/enabled state, clicking) is now shared. Subclasses only implement GetPageAsync() to provide their specific IPage source.

Copy link
Owner

Choose a reason for hiding this comment

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

The Page and Browser properties can be removed from this class, now that they are no longer being directly referenced by the test service

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed unused Page and Browser properties in commit 11e9d21. Only the IBrowserProvider.TestContext explicit interface implementation remains.

/// Gets the browser page/document instance for interacting with the UI.
/// The actual type depends on the browser implementation (e.g., Playwright IPage, WebView2 CoreWebView2).
/// </summary>
object? BrowserPage { get; }
Copy link
Owner

Choose a reason for hiding this comment

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

Since this is also used to pass the debugger port in BlazorWebView cases, it shouldn't be called "BrowserPage"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed to TestContext in commit 11e9d21. Now clearly indicates it can be either an IPage or a port number depending on implementation.

// UI-related host services
services.AddSingleton(mainWindow);
services.AddSingleton<IBrowserProvider, WebView2BrowserProvider>();
services.AddHostedService<WebView2TestConfigurator>(); // Must run before BlazorWindowServicesSetter
Copy link
Owner

Choose a reason for hiding this comment

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

Combine WebView2TestConfigurator and BlazorWindowServicesSetter into one class. They're both basically doing the same thing, setting up the BlazorWebView.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Combined in commit 11e9d21. BlazorWindowServicesSetter now handles both WebView2 configuration (if test mode enabled) and service provider setup. Removed separate WebView2TestConfigurator class.

{
private readonly IConfiguration _configuration;

public WebView2BrowserProvider(IConfiguration configuration)
Copy link
Owner

Choose a reason for hiding this comment

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

Import the TestingSettings type and use it, instead of accessing IConfiguration like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in commit 11e9d21. Now uses IOptions<TestingSettings> and accesses _testingSettings.WebViewRemoteDebuggingPort.

private readonly BlazorWebView _browser;
private readonly IConfiguration _configuration;

public WebView2TestConfigurator(MainWindow mainWindow, IConfiguration configuration)
Copy link
Owner

Choose a reason for hiding this comment

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

Also here, use the TestingSettings type instead of raw IConfiguration access.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in commit 11e9d21. BlazorWindowServicesSetter now uses IOptions<TestingSettings> instead of IConfiguration.

await _browser.Dispatcher.InvokeAsync(async () =>
{
// Create per-instance user data folder to avoid collisions
string userDataFolder = System.IO.Path.Combine(
Copy link
Owner

Choose a reason for hiding this comment

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

I don't think it's necessary to create a unique user data folder for this. We don't when running normally.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed in commit 11e9d21. Now passes null for user data folder, using default location like normal operation.

Copy link
Owner

Choose a reason for hiding this comment

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

See comment in HeadlessUITestService. Logic in these two services should be shared, not duplicated. Everything except for how the IPage is acquired.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored in commit 11e9d21. Both services now extend UITestServiceBase which contains all shared logic. Only GetPageAsync() differs between implementations.

_testEndpoint.CreateTestServiceAsync<ApplicationTestService>);

// Choose UI test service based on host type
if (_settings.ExePath.Contains("Headless", StringComparison.OrdinalIgnoreCase))
Copy link
Owner

Choose a reason for hiding this comment

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

Instead of basing this on the ExePath, let's add a setting to AdaptiveRemoteHostSettings, so that the test can control which kind of UI service it expects.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added UIServiceType enum and UIService property to AdaptiveRemoteHostSettings in commit 11e9d21. Test controls which service type is expected. Defaults to Headless for "Headless" in path, otherwise BlazorWebView.

@jodavis jodavis marked this pull request as ready for review January 7, 2026 19:25
@github-actions
Copy link

github-actions bot commented Jan 7, 2026

Test Results

306 tests   304 ✅  47s ⏱️
  3 suites    2 💤
  3 files      0 ❌

Results for commit d806beb.

♻️ This comment has been updated with latest results.

Copilot AI requested a review from jodavis January 7, 2026 19:33
Copilot AI and others added 7 commits January 9, 2026 16:03
Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
…c number

Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
- Added WebViewRemoteDebuggingPort setting to TestingSettings
- Created WebView2TestConfigurator to configure WebView2 environment with remote debugging
- Created WebView2BrowserProvider to provide remote debugging port to test services
- Created BlazorWebViewUITestService that connects to WebView2 via Playwright
- Updated AdaptiveRemoteHost to choose HeadlessUITestService or BlazorWebViewUITestService based on host type
- Tests can now drive UI on both Headless and WebView2-based hosts

Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
…configuration

- Created UITestServiceBase abstract class to share logic between Headless and BlazorWebView implementations
- Removed unused Page and Browser properties from PlaywrightBrowserLifetimeService
- Renamed IBrowserProvider.BrowserPage to TestContext for better clarity
- Combined WebView2TestConfigurator into BlazorWindowServicesSetter
- Updated WebView2BrowserProvider and BlazorWindowServicesSetter to use TestingSettings instead of IConfiguration
- Removed unique user data folder creation (not necessary)
- Added UIServiceType enum and property to AdaptiveRemoteHostSettings
- Updated AdaptiveRemoteHost to use UIService property instead of ExePath check
- Made TestingSettings public for cross-assembly access

Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
@jodavis jodavis force-pushed the copilot/use-playwright-for-e2e-tests branch from 11e9d21 to d806beb Compare January 10, 2026 01:48
@jodavis jodavis merged commit 30c1639 into main Jan 10, 2026
2 checks passed
@jodavis jodavis deleted the copilot/use-playwright-for-e2e-tests branch January 10, 2026 02:02
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.

Use Playwright to drive UI in E2E tests

3 participants