From 590236d897bf2a2df53a945bcbbdcce58413389e Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 1 Nov 2023 12:09:18 +0700 Subject: [PATCH 01/12] add key id to jwt created for cookie, this fixes a validation issue where the jwt is missing the key id --- backend/LexBoxApi/Auth/JwtTicketDataFormat.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/LexBoxApi/Auth/JwtTicketDataFormat.cs b/backend/LexBoxApi/Auth/JwtTicketDataFormat.cs index 0de3b4447..8c5242cea 100644 --- a/backend/LexBoxApi/Auth/JwtTicketDataFormat.cs +++ b/backend/LexBoxApi/Auth/JwtTicketDataFormat.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using LexCore.Auth; @@ -50,6 +51,8 @@ public static string ConvertAuthTicketToJwt(AuthenticationTicket data, var jwtDate = DateTime.UtcNow; _jwtSecurityTokenHandler.MapInboundClaims = jwtBearerOptions.MapInboundClaims; var claimsIdentity = new ClaimsIdentity(data.Principal.Claims, data.Principal.Identity?.AuthenticationType); + var keyId = Guid.NewGuid().ToString().GetHashCode().ToString("x", CultureInfo.InvariantCulture); + claimsIdentity.AddClaim(new Claim(JwtRegisteredClaimNames.Jti, keyId)); //there may already be an audience claim, we want to reuse that if it exists, if not fallback to the default audience var audience = DetermineAudience(claimsIdentity) ?? jwtBearerOptions.TokenValidationParameters.ValidAudience; var securityTokenDescriptor = new SecurityTokenDescriptor From f362850bd3846ebba5de4dab9ab9bc5e1c3c5aff Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 1 Nov 2023 13:32:08 +0700 Subject: [PATCH 02/12] only attempt to deploy on develop branch builds --- .github/workflows/develop-api.yaml | 1 + .github/workflows/develop-ui.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/develop-api.yaml b/.github/workflows/develop-api.yaml index 52fd0d38d..affdc8564 100644 --- a/.github/workflows/develop-api.yaml +++ b/.github/workflows/develop-api.yaml @@ -39,6 +39,7 @@ jobs: version: ${{ needs.set-version.outputs.version }} deploy-api: name: Deploy API + if: ${{github.ref == 'refs/heads/develop'}} needs: [ build-api, set-version ] uses: ./.github/workflows/deploy.yaml secrets: inherit diff --git a/.github/workflows/develop-ui.yaml b/.github/workflows/develop-ui.yaml index 406c67b0a..5341d33dd 100644 --- a/.github/workflows/develop-ui.yaml +++ b/.github/workflows/develop-ui.yaml @@ -35,6 +35,7 @@ jobs: version: ${{ needs.set-version.outputs.version }} deploy-api: name: Deploy API + if: ${{github.ref == 'refs/heads/develop'}} needs: [ build-ui, set-version ] uses: ./.github/workflows/deploy.yaml secrets: inherit From 7f436e5ee9716ec7728d3d77285fb1d54647ade6 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Fri, 27 Oct 2023 11:38:03 +0200 Subject: [PATCH 03/12] Add loading overlay while hydrating --- frontend/src/routes/+layout.svelte | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 75bd9b4bc..97a1ce91f 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -11,6 +11,8 @@ import { Duration } from '$lib/util/time'; import { browser } from '$app/environment'; import t from '$lib/i18n'; + import { onMount } from 'svelte'; + import { blur } from 'svelte/transition'; export let data: LayoutData; const { page, updated } = getStores(); @@ -22,6 +24,9 @@ notifyWarning($t('notifications.update_detected'), Duration.Long); } } + + let unhydrated = true; + onMount(() => unhydrated = false); @@ -34,6 +39,13 @@ {/if} +{#if unhydrated} +
+ +
+
+{/if} +
From e5601d2722c2e501d0ad0c294871ef3f54b65a0f Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Fri, 27 Oct 2023 11:41:47 +0200 Subject: [PATCH 04/12] Use skeleton/hide elements during hydration Instead of covering with overlay, because (1) it's weird to cover up a loaded page (2) it doesn't block keypresses --- frontend/src/lib/app.postcss | 43 ++++++++++++++++++++++++++++++ frontend/src/routes/+layout.svelte | 10 +------ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/frontend/src/lib/app.postcss b/frontend/src/lib/app.postcss index 3c3df868a..b835f2f4f 100644 --- a/frontend/src/lib/app.postcss +++ b/frontend/src/lib/app.postcss @@ -124,3 +124,46 @@ table tr:nth-last-child(-n + 2) .dropdown { transition: background-color 0.5s; } } + +.unhydrated { + .input, input, button { + visibility: hidden; + } + + form { + & > * { + visibility: hidden; + } + + position: relative; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + @apply w-full h-full; + @apply rounded-xl; + + background-image: linear-gradient(110deg, var(--tw-gradient-stops)); + @apply from-base-300; + @apply via-neutral-600; + @apply to-base-300; + background-size: 400% 400%; + animation: gradient 5s ease-in-out infinite; + + @keyframes gradient { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } + } + + } + } +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 97a1ce91f..f8e7d68bb 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -12,7 +12,6 @@ import { browser } from '$app/environment'; import t from '$lib/i18n'; import { onMount } from 'svelte'; - import { blur } from 'svelte/transition'; export let data: LayoutData; const { page, updated } = getStores(); @@ -39,14 +38,7 @@ {/if} -{#if unhydrated} -
- -
-
-{/if} - -
+
From fb56e0c403a8748f80adbb2ac459c0f07894b944 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Fri, 27 Oct 2023 11:42:46 +0200 Subject: [PATCH 05/12] Wait for Load instead of NetworkIdle We don't have to wait until NetworkIdle/Hydrated, because the app blocks user interaction automatically. --- backend/Testing/Browser/Page/BasePage.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/Testing/Browser/Page/BasePage.cs b/backend/Testing/Browser/Page/BasePage.cs index 0eec14d9c..ce167b5f7 100644 --- a/backend/Testing/Browser/Page/BasePage.cs +++ b/backend/Testing/Browser/Page/BasePage.cs @@ -36,11 +36,11 @@ public async Task WaitFor() { if (Url is not null) { - await Page.WaitForURLAsync(Url, new() { WaitUntil = WaitUntilState.NetworkIdle }); + await Page.WaitForURLAsync(Url, new() { WaitUntil = WaitUntilState.Load }); } else { - await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + await Page.WaitForLoadStateAsync(LoadState.Load); } await Task.WhenAll(TestLocators.Select(l => l.WaitForAsync())); return (T)this; From 1b620a3a826c0f9580e911966ef936f981dcae3a Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Fri, 27 Oct 2023 13:37:32 +0200 Subject: [PATCH 06/12] Use simpler hydration/loading animation --- frontend/src/lib/app.postcss | 58 +++++++++++++----------------------- 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/frontend/src/lib/app.postcss b/frontend/src/lib/app.postcss index b835f2f4f..6f3c5b044 100644 --- a/frontend/src/lib/app.postcss +++ b/frontend/src/lib/app.postcss @@ -126,44 +126,28 @@ table tr:nth-last-child(-n + 2) .dropdown { } .unhydrated { - .input, input, button { - visibility: hidden; + .input, + input, + button { + visibility: hidden; + } + + form { + & > * { + visibility: hidden; } - form { - & > * { - visibility: hidden; - } - - position: relative; - - &::before { - content: ''; - position: absolute; - top: 0; - left: 0; - @apply w-full h-full; - @apply rounded-xl; - - background-image: linear-gradient(110deg, var(--tw-gradient-stops)); - @apply from-base-300; - @apply via-neutral-600; - @apply to-base-300; - background-size: 400% 400%; - animation: gradient 5s ease-in-out infinite; - - @keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } - } - - } + position: relative; + + &::before { + content: ''; + @apply loading loading-ring bg-primary; + @apply h-32 max-h-full w-auto; + + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); } + } } From d11d23274219e463971f8d919e76620af261bf59 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 31 Oct 2023 11:55:29 +0100 Subject: [PATCH 07/12] Rename variable to 'hydrating' --- frontend/src/lib/app.postcss | 2 +- frontend/src/routes/+layout.svelte | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/app.postcss b/frontend/src/lib/app.postcss index 6f3c5b044..903448514 100644 --- a/frontend/src/lib/app.postcss +++ b/frontend/src/lib/app.postcss @@ -125,7 +125,7 @@ table tr:nth-last-child(-n + 2) .dropdown { } } -.unhydrated { +.hydrating { .input, input, button { diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index f8e7d68bb..eff79487b 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -24,8 +24,8 @@ } } - let unhydrated = true; - onMount(() => unhydrated = false); + let hydrating = true; + onMount(() => hydrating = false); @@ -38,7 +38,7 @@ {/if} -
+
From 08f4ee19a149ca6ba5721127bc297cb275c5dd26 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Thu, 2 Nov 2023 16:59:36 +0100 Subject: [PATCH 08/12] Fix welcome message resizing on invalid login credentials --- frontend/src/routes/(unauthenticated)/login/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/routes/(unauthenticated)/login/+page.svelte b/frontend/src/routes/(unauthenticated)/login/+page.svelte index d057ecf3c..5a198194c 100644 --- a/frontend/src/routes/(unauthenticated)/login/+page.svelte +++ b/frontend/src/routes/(unauthenticated)/login/+page.svelte @@ -37,7 +37,7 @@
-
+
From 5d20f94856115172e7c6aaab4be18860a8366b21 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 31 Oct 2023 11:24:03 +0100 Subject: [PATCH 09/12] Make Playwright throw exceptions for 500s and test --- backend/Testing/Browser/Base/PageTest.cs | 33 ++++++++++++ .../Base/UnexpectedResponseException.cs | 23 +++++++++ backend/Testing/Browser/SandboxPageTests.cs | 51 ++++++++++++++++--- .../(unauthenticated)/sandbox/+page.svelte | 6 +++ 4 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 backend/Testing/Browser/Base/UnexpectedResponseException.cs diff --git a/backend/Testing/Browser/Base/PageTest.cs b/backend/Testing/Browser/Base/PageTest.cs index 7680f897f..846a5ad20 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 deferred exception that was "thrown" in a sub-thread, and returns it + /// or throws if no exception of the given type is found. + /// + public UnexpectedResponseException ExpectDeferredException() + { + var exception = DeferredExceptions.ShouldHaveSingleItem(); + DeferredExceptions.Clear(); + return exception; + } + + public void ExpectNoDeferredExceptions() + { + DeferredExceptions.ShouldBeEmpty(); + } public virtual async Task InitializeAsync() { @@ -39,6 +59,14 @@ await Context.Tracing.StartAsync(new() Sources = true }); } + + Context.Response += (_, response) => + { + if (response.Status >= (int)HttpStatusCode.InternalServerError) + { + DeferredExceptions.Add(new UnexpectedResponseException(response)); + } + }; } public virtual async Task DisposeAsync() @@ -52,6 +80,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 000000000..7e4db97ba --- /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 d2b7bf8d2..e0b0dbacc 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,51 @@ 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(); + await Expect(Page.Locator(".modal-box.bg-error:has-text('Internal Server Error (500)')")).ToBeVisibleAsync(); } } diff --git a/frontend/src/routes/(unauthenticated)/sandbox/+page.svelte b/frontend/src/routes/(unauthenticated)/sandbox/+page.svelte index 0b84a412a..e4f87a48d 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

From 0c2097f39c1bfa1aad4ea46fbfd834620f1e0f25 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 31 Oct 2023 11:34:47 +0100 Subject: [PATCH 10/12] Show error dialog for 500 featch responses --- frontend/src/hooks.client.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/src/hooks.client.ts b/frontend/src/hooks.client.ts index 5160b2bd3..30e638bd0 100644 --- a/frontend/src/hooks.client.ts +++ b/frontend/src/hooks.client.ts @@ -63,11 +63,18 @@ function shouldTryAutoReload(updateDetected: boolean): boolean { */ handleFetch(async ({ fetch, args }) => { const response = await traceFetch(() => fetch(...args)); + if (response.status === 401 && location.pathname !== '/login') { throw redirect(307, '/logout'); } + + if (response.status >= 500) { + throw new Error(`Unexpected response: ${response.statusText} (${response.status}). URL: ${response.url}.`); + } + if (response.headers.get('lexbox-refresh-jwt') == 'true') { await invalidate(USER_LOAD_KEY); } + return response; }); From fb5bc614f61ad0c91b96b15db14dc4b497651c24 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Mon, 6 Nov 2023 15:12:02 +0100 Subject: [PATCH 11/12] Fix Playwright tests --- backend/Testing/Browser/EmailWorkflowTests.cs | 6 +++--- backend/Testing/Browser/Page/AuthenticatedBasePage.cs | 6 ++++++ backend/Testing/Browser/Page/BasePage.cs | 6 ++++-- backend/Testing/Browser/SandboxPageTests.cs | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/Testing/Browser/EmailWorkflowTests.cs b/backend/Testing/Browser/EmailWorkflowTests.cs index 0ca3574b0..5e0ef5575 100644 --- a/backend/Testing/Browser/EmailWorkflowTests.cs +++ b/backend/Testing/Browser/EmailWorkflowTests.cs @@ -32,12 +32,12 @@ public async Task RegisterVerifyUpdateVerifyEmailAdress() await userPage.EmailVerificationAlert.AssertSuccessfullyVerified(); - // Step: Verify verification alert is gone - await userPage.Page.ReloadAsync(); // So we don't have to wait ~10 seconds for the alert to disappear - await userPage.WaitFor(); + // Step: Verify verification alert goes away on navigation + await userPage.GoHome(); await userPage.EmailVerificationAlert.AssertGone(); // Step: Request new e-mail address + await userPage.Goto(); var newMailinatorId = Guid.NewGuid().ToString(); var newEmail = $"{newMailinatorId}@mailinator.com"; await userPage.FillEmail(newEmail); diff --git a/backend/Testing/Browser/Page/AuthenticatedBasePage.cs b/backend/Testing/Browser/Page/AuthenticatedBasePage.cs index 5405ceed6..5f1411b2d 100644 --- a/backend/Testing/Browser/Page/AuthenticatedBasePage.cs +++ b/backend/Testing/Browser/Page/AuthenticatedBasePage.cs @@ -11,4 +11,10 @@ public AuthenticatedBasePage(IPage page, string url, ILocator testLocator) { EmailVerificationAlert = new EmailVerificationAlert(Page); } + + public async Task GoHome() + { + await Page.Locator(".breadcrumbs").GetByRole(AriaRole.Link, new() { Name = "Home" }).ClickAsync(); + return await new UserDashboardPage(Page).WaitFor(); + } } diff --git a/backend/Testing/Browser/Page/BasePage.cs b/backend/Testing/Browser/Page/BasePage.cs index ce167b5f7..99dbd6308 100644 --- a/backend/Testing/Browser/Page/BasePage.cs +++ b/backend/Testing/Browser/Page/BasePage.cs @@ -1,3 +1,4 @@ +using System.Text.RegularExpressions; using Microsoft.Playwright; using Shouldly; @@ -8,6 +9,7 @@ public abstract class BasePage where T : BasePage public IPage Page { get; private set; } public string? Url { get; protected set; } protected ILocator[] TestLocators { get; } + private Regex? UrlPattern => Url is not null ? new Regex($"{Regex.Escape(Url)}($|\\?|#)") : null; public BasePage(IPage page, string? url, ILocator testLocator) : this(page, url, new[] { testLocator }) @@ -34,9 +36,9 @@ public virtual async Task Goto() public async Task WaitFor() { - if (Url is not null) + if (UrlPattern is not null) { - await Page.WaitForURLAsync(Url, new() { WaitUntil = WaitUntilState.Load }); + await Page.WaitForURLAsync(UrlPattern, new() { WaitUntil = WaitUntilState.Load }); } else { diff --git a/backend/Testing/Browser/SandboxPageTests.cs b/backend/Testing/Browser/SandboxPageTests.cs index e0b0dbacc..9d182a753 100644 --- a/backend/Testing/Browser/SandboxPageTests.cs +++ b/backend/Testing/Browser/SandboxPageTests.cs @@ -53,6 +53,6 @@ await Page.RunAndWaitForResponseAsync(async () => await Page.GetByText("Fetch 500").ClickAsync(); }, "/api/testing/test500NoException"); ExpectDeferredException(); - await Expect(Page.Locator(".modal-box.bg-error:has-text('Internal Server Error (500)')")).ToBeVisibleAsync(); + await Expect(Page.Locator(".modal-box.bg-error:text-matches('Unexpected response:.*(500)', 'g')")).ToBeVisibleAsync(); } } From ae1faaf5884bb3b890f64e11e1b3ec5a43c7b7e0 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Mon, 6 Nov 2023 16:34:18 +0100 Subject: [PATCH 12/12] Remove irrelevant test --- backend/Testing/Browser/SandboxPageTests.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/backend/Testing/Browser/SandboxPageTests.cs b/backend/Testing/Browser/SandboxPageTests.cs index 9d182a753..bbcf16f13 100644 --- a/backend/Testing/Browser/SandboxPageTests.cs +++ b/backend/Testing/Browser/SandboxPageTests.cs @@ -30,20 +30,6 @@ await Context.RunAndWaitForPageAsync(async () => 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() {