diff --git a/backend/Testing/Browser/Base/PageTest.cs b/backend/Testing/Browser/Base/PageTest.cs index 7680f897fe..0f66dc83c5 100644 --- a/backend/Testing/Browser/Base/PageTest.cs +++ b/backend/Testing/Browser/Base/PageTest.cs @@ -17,6 +17,11 @@ public class PageTest : IAsyncLifetime public IPage Page => _fixture.Page; public IBrowser Browser => _fixture.Browser; public IBrowserContext Context => _fixture.Context; + /// + /// Exceptions that are deferred until the end of the test, because they can't + /// be cleanly thrown in sub-threads. + /// + private List DeferredExceptions { get; } = new(); public PageTest() { @@ -26,6 +31,21 @@ public PageTest() public ILocatorAssertions Expect(ILocator locator) => Assertions.Expect(locator); public IPageAssertions Expect(IPage page) => Assertions.Expect(page); public IAPIResponseAssertions Expect(IAPIResponse response) => Assertions.Expect(response); + /// + /// Consumes a defferred exception that was "thrown" in a sub-thread, and returns it + /// or throws if no exception of the given type is found. + /// + public Exception ExpectDeferredException() where TException : Exception + { + var exception = DeferredExceptions.FirstOrDefault(e => e is TException); + exception.ShouldNotBeNull($"Expected deferred exception of type {typeof(TException).FullName} was not found."); + DeferredExceptions.Remove(exception); + return exception; + } + public void ExpectNoDeferredExceptions() + { + DeferredExceptions.ShouldBeEmpty(); + } public virtual async Task InitializeAsync() { @@ -39,6 +59,19 @@ await Context.Tracing.StartAsync(new() Sources = true }); } + + Context.Response += (_, response) => + { + if (response.Status >= (int)HttpStatusCode.InternalServerError) + { + DeferredExceptions.Add(new UnexpectedResponseException(response)); + } + }; + + Context.Request += (_, request) => + { + Console.WriteLine(request.IsNavigationRequest); + }; } public virtual async Task DisposeAsync() @@ -52,6 +85,11 @@ public virtual async Task DisposeAsync() } await _fixture.DisposeAsync(); + + if (DeferredExceptions.Any()) + { + throw new AggregateException(DeferredExceptions); + } } static readonly HttpClient HttpClient = new HttpClient(); diff --git a/backend/Testing/Browser/Base/UnexpectedResponseException.cs b/backend/Testing/Browser/Base/UnexpectedResponseException.cs new file mode 100644 index 0000000000..7e4db97ba6 --- /dev/null +++ b/backend/Testing/Browser/Base/UnexpectedResponseException.cs @@ -0,0 +1,23 @@ +using System.Text.RegularExpressions; +using Microsoft.Playwright; + +namespace Testing.Browser.Base; + +public partial class UnexpectedResponseException : SystemException +{ + public static string MaskUrl(string url) + { + return JwtRegex().Replace(url, "*****"); + } + + public UnexpectedResponseException(IResponse response) + : this(response.StatusText, response.Status, response.Url) + { + } + + public UnexpectedResponseException(string statusText, int statusCode, string url) + : base($"Unexpected response: {statusText} ({statusCode}). URL: {MaskUrl(url)}.") { } + + [GeneratedRegex("[A-Za-z0-9-_]{10,}\\.[A-Za-z0-9-_]{20,}\\.[A-Za-z0-9-_]{10,}")] + private static partial Regex JwtRegex(); +} diff --git a/backend/Testing/Browser/SandboxPageTests.cs b/backend/Testing/Browser/SandboxPageTests.cs index d2b7bf8d2f..d74e77e410 100644 --- a/backend/Testing/Browser/SandboxPageTests.cs +++ b/backend/Testing/Browser/SandboxPageTests.cs @@ -1,4 +1,4 @@ -using Shouldly; +using Microsoft.Playwright; using Testing.Browser.Base; using Testing.Browser.Page; @@ -8,16 +8,50 @@ namespace Testing.Browser; public class SandboxPageTests : PageTest { [Fact] - public async Task Goto500Works() + public async Task CatchGoto500InSameTab() + { + + await new SandboxPage(Page).Goto(); + await Page.RunAndWaitForResponseAsync(async () => + { + await Page.GetByText("Goto 500 page").ClickAsync(); + }, "/api/testing/test500NoException"); + ExpectDeferredException(); + } + + [Fact] + public async Task CatchGoto500InNewTab() { await new SandboxPage(Page).Goto(); - var request = await Page.RunAndWaitForRequestFinishedAsync(async () => + await Context.RunAndWaitForPageAsync(async () => { - await Page.GetByText("goto 500 page").ClickAsync(); + await Page.GetByText("goto 500 new tab").ClickAsync(); }); - var response = await request.ResponseAsync(); - response.ShouldNotBeNull(); - response.Ok.ShouldBeFalse(); - response.Status.ShouldBe(500); + ExpectDeferredException(); + } + + [Fact(Skip = "Playwright doesn't catch the document load request of pages opened with Ctrl+Click")] + public async Task CatchGoto500InNewTabWithCtrl() + { + await new SandboxPage(Page).Goto(); + await Context.RunAndWaitForPageAsync(async () => + { + await Page.GetByText("Goto 500 page").ClickAsync(new() + { + Modifiers = new[] { KeyboardModifier.Control }, + }); + }); + ExpectDeferredException(); + } + + [Fact] + public async Task CatchFetch500() + { + await new SandboxPage(Page).Goto(); + await Page.RunAndWaitForResponseAsync(async () => + { + await Page.GetByText("Fetch 500").ClickAsync(); + }, "/api/testing/test500NoException"); + ExpectDeferredException(); } } diff --git a/frontend/src/routes/(unauthenticated)/sandbox/+page.svelte b/frontend/src/routes/(unauthenticated)/sandbox/+page.svelte index 0b84a412aa..e4f87a48d3 100644 --- a/frontend/src/routes/(unauthenticated)/sandbox/+page.svelte +++ b/frontend/src/routes/(unauthenticated)/sandbox/+page.svelte @@ -5,11 +5,17 @@ function uploadFinished(): void { alert('upload done!'); } + + async function fetch500(): Promise { + return fetch('/api/testing/test500NoException'); + }

Sandbox