Skip to content

Commit 78112e1

Browse files
committed
Stabilize browser suite bootstrap and tooltip waits
1 parent 5442cfb commit 78112e1

7 files changed

Lines changed: 169 additions & 66 deletions

File tree

tests/PrompterOne.Web.UITests.Editor/Editor/EditorThemeFlowTests.cs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,20 @@ await Expect(page.Locator("html")).ToHaveAttributeAsync(
4949
var emotionTrigger = page.GetByTestId(UiTestIds.Editor.EmotionTrigger);
5050
var emotionMenu = page.GetByTestId(UiTestIds.Editor.MenuEmotion);
5151
var motivationalEmotion = page.GetByTestId(UiTestIds.Editor.EmotionMotivational);
52-
var tooltip = page.GetByTestId(UiTestIds.Editor.ToolbarTooltip)
53-
.Filter(new() { HasTextString = BrowserTestConstants.EditorFlow.MotivationalEmotionTooltipText });
52+
var tooltip = EditorTooltipDriver.GetToolbarTooltip(page, BrowserTestConstants.EditorFlow.MotivationalEmotionTooltipText);
5453

5554
await emotionTrigger.ClickAsync();
5655
await Expect(emotionMenu).ToBeVisibleAsync();
5756
await Expect(motivationalEmotion).ToBeVisibleAsync();
5857

5958
await motivationalEmotion.HoverAsync();
60-
await page.WaitForTimeoutAsync(BrowserTestConstants.EditorFlow.TooltipSettleDelayMs);
61-
await Expect(tooltip).ToBeVisibleAsync();
59+
await EditorTooltipDriver.WaitUntilFullyVisibleAsync(page, tooltip, BrowserTestConstants.EditorFlow.MotivationalEmotionTooltipText);
6260

6361
var menuBackground = await ReadCssColorAsync(emotionMenu, BackgroundColorProperty);
6462
var menuItemColor = await ReadCssColorAsync(motivationalEmotion, ColorProperty);
6563
var tooltipBackground = await ReadCssColorAsync(tooltip, BackgroundColorProperty);
6664
var tooltipColor = await ReadCssColorAsync(tooltip, ColorProperty);
67-
var tooltipOpacity = await ReadOpacityAsync(tooltip);
65+
var tooltipOpacity = await EditorTooltipDriver.ReadOpacityAsync(tooltip);
6866

6967
await Assert.That(await motivationalEmotion.GetAttributeAsync("title")).IsNull();
7068
await Assert.That(await motivationalEmotion.GetAttributeAsync("aria-label")).IsEqualTo(BrowserTestConstants.EditorFlow.MotivationalEmotionTooltipText);
@@ -116,9 +114,4 @@ await locator.EvaluateAsync<CssColor>(
116114
""",
117115
propertyName);
118116

119-
private static async Task<double> ReadOpacityAsync(ILocator locator) =>
120-
await locator.EvaluateAsync<double>(
121-
"""
122-
element => Number.parseFloat(getComputedStyle(element).opacity)
123-
""");
124117
}

tests/PrompterOne.Web.UITests.Editor/Editor/EditorToolbarTooltipFlowTests.cs

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,16 @@ public Task EditorScreen_ToolbarTooltip_AppearsOnlyAfterDelay() =>
2424
await OpenEditorAsync(page);
2525

2626
var emotionTrigger = page.GetByTestId(UiTestIds.Editor.EmotionTrigger);
27-
var tooltip = GetTooltipLocator(page, BrowserTestConstants.EditorFlow.EmotionTooltipText);
27+
var tooltip = EditorTooltipDriver.GetToolbarTooltip(page, BrowserTestConstants.EditorFlow.EmotionTooltipText);
2828

2929
await emotionTrigger.HoverAsync();
3030
await page.WaitForTimeoutAsync(BrowserTestConstants.EditorFlow.TooltipEarlyCheckDelayMs);
3131

32-
await Assert.That(await ReadOpacityAsync(tooltip)).IsBetween(0, BrowserTestConstants.EditorFlow.MaximumEarlyTooltipOpacity);
32+
await Assert.That(await EditorTooltipDriver.ReadOpacityAsync(tooltip)).IsBetween(0, BrowserTestConstants.EditorFlow.MaximumEarlyTooltipOpacity);
3333

3434
await page.WaitForTimeoutAsync(
3535
BrowserTestConstants.EditorFlow.TooltipSettleDelayMs - BrowserTestConstants.EditorFlow.TooltipEarlyCheckDelayMs);
36-
await Expect(tooltip).ToBeVisibleAsync();
37-
await Expect(tooltip).ToHaveTextAsync(BrowserTestConstants.EditorFlow.EmotionTooltipText);
36+
await EditorTooltipDriver.WaitUntilFullyVisibleAsync(page, tooltip, BrowserTestConstants.EditorFlow.EmotionTooltipText);
3837

3938
await UiScenarioArtifacts.CapturePageAsync(
4039
page,
@@ -55,19 +54,18 @@ public Task EditorScreen_DropdownTooltip_StaysOutsideMenuAndDoesNotBlockAction()
5554
var emotionTrigger = page.GetByTestId(UiTestIds.Editor.EmotionTrigger);
5655
var emotionMenu = page.GetByTestId(UiTestIds.Editor.MenuEmotion);
5756
var motivationalEmotion = page.GetByTestId(UiTestIds.Editor.EmotionMotivational);
58-
var tooltip = GetTooltipLocator(page, BrowserTestConstants.EditorFlow.MotivationalEmotionTooltipText);
57+
var tooltip = EditorTooltipDriver.GetToolbarTooltip(page, BrowserTestConstants.EditorFlow.MotivationalEmotionTooltipText);
5958

6059
await emotionTrigger.ClickAsync();
6160
await Expect(emotionMenu).ToBeVisibleAsync();
6261
await motivationalEmotion.HoverAsync();
6362
await page.WaitForTimeoutAsync(BrowserTestConstants.EditorFlow.TooltipEarlyCheckDelayMs);
6463

65-
await Assert.That(await ReadOpacityAsync(tooltip)).IsBetween(0, BrowserTestConstants.EditorFlow.MaximumEarlyTooltipOpacity);
64+
await Assert.That(await EditorTooltipDriver.ReadOpacityAsync(tooltip)).IsBetween(0, BrowserTestConstants.EditorFlow.MaximumEarlyTooltipOpacity);
6665

6766
await page.WaitForTimeoutAsync(
6867
BrowserTestConstants.EditorFlow.TooltipSettleDelayMs - BrowserTestConstants.EditorFlow.TooltipEarlyCheckDelayMs);
69-
await Expect(tooltip).ToBeVisibleAsync();
70-
await Expect(tooltip).ToHaveTextAsync(BrowserTestConstants.EditorFlow.MotivationalEmotionTooltipText);
68+
await EditorTooltipDriver.WaitUntilFullyVisibleAsync(page, tooltip, BrowserTestConstants.EditorFlow.MotivationalEmotionTooltipText);
7169

7270
var tooltipSurface = await ReadTooltipSurfaceAsync(tooltip);
7371
await Assert.That(tooltipSurface.BorderAlpha >= BrowserTestConstants.EditorFlow.MinimumTooltipSurfaceBorderAlpha).IsTrue();
@@ -99,12 +97,12 @@ public Task EditorScreen_ToolbarTooltip_StaysInsideViewport() =>
9997
await OpenEditorAsync(page);
10098

10199
var emotionTrigger = page.GetByTestId(UiTestIds.Editor.EmotionTrigger);
102-
var tooltip = GetTooltipLocator(page, BrowserTestConstants.EditorFlow.EmotionTooltipText);
100+
var tooltip = EditorTooltipDriver.GetToolbarTooltip(page, BrowserTestConstants.EditorFlow.EmotionTooltipText);
103101

104102
await emotionTrigger.HoverAsync();
105103
await page.WaitForTimeoutAsync(BrowserTestConstants.EditorFlow.TooltipSettleDelayMs);
106104

107-
await Expect(tooltip).ToBeVisibleAsync();
105+
await EditorTooltipDriver.WaitUntilFullyVisibleAsync(page, tooltip, BrowserTestConstants.EditorFlow.EmotionTooltipText);
108106

109107
var tooltipBounds = await ReadBoundsAsync(tooltip);
110108
var viewportHeight = await ReadViewportHeightAsync(page);
@@ -127,16 +125,6 @@ private static double CalculateIntersectionArea(ElementBounds left, ElementBound
127125
return overlapWidth * overlapHeight;
128126
}
129127

130-
private static ILocator GetTooltipLocator(IPage page, string tooltipText) =>
131-
page.GetByTestId(UiTestIds.Editor.ToolbarTooltip)
132-
.Filter(new() { HasTextString = tooltipText });
133-
134-
private static async Task<double> ReadOpacityAsync(ILocator locator) =>
135-
await locator.EvaluateAsync<double>(
136-
"""
137-
element => Number.parseFloat(getComputedStyle(element).opacity)
138-
""");
139-
140128
private static async Task OpenEditorAsync(IPage page)
141129
{
142130
await page.GotoAsync(BrowserTestConstants.Routes.EditorDemo);

tests/PrompterOne.Web.UITests.Reader/Teleprompter/TeleprompterAlignmentTooltipFlowTests.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,7 @@ await Expect(page.GetByTestId(UiTestIds.Teleprompter.Page))
9595

9696
private static async Task AssertTooltipDismissesAsync(IPage page, ILocator trigger, ILocator tooltip)
9797
{
98-
await page.Mouse.MoveAsync(
99-
BrowserTestConstants.TooltipAuditFlow.ClearHoverX,
100-
BrowserTestConstants.TooltipAuditFlow.ClearHoverY);
98+
await page.GetByTestId(UiTestIds.Teleprompter.Stage).HoverAsync();
10199
await Expect(tooltip).ToBeHiddenAsync(
102100
new() { Timeout = BrowserTestConstants.TeleprompterFlow.TooltipDismissTimeoutMs });
103101

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using Microsoft.Playwright;
2+
using PrompterOne.Shared.Contracts;
3+
using static Microsoft.Playwright.Assertions;
4+
5+
namespace PrompterOne.Web.UITests;
6+
7+
public sealed partial class StandaloneAppFixture
8+
{
9+
private static readonly (string Route, string PageTestId)[] RuntimeWarmupRoutes =
10+
[
11+
(BrowserTestConstants.Routes.Library, UiTestIds.Library.Page),
12+
(BrowserTestConstants.Routes.Settings, UiTestIds.Settings.Page),
13+
(BrowserTestConstants.Routes.LearnDemo, UiTestIds.Learn.Page),
14+
(BrowserTestConstants.Routes.TeleprompterDemo, UiTestIds.Teleprompter.Page),
15+
(BrowserTestConstants.Routes.GoLiveDemo, UiTestIds.GoLive.Page),
16+
(BrowserTestConstants.Routes.EditorDemo, UiTestIds.Editor.Page)
17+
];
18+
19+
private static async Task InitializeContextAsync(IBrowserContext context, string baseAddress)
20+
{
21+
await context.AddInitScriptAsync(BrowserTestLibrarySeedData.CreateInitializationScript());
22+
await context.AddInitScriptAsync(UiTestHostConstants.RuntimeTelemetryHarnessInitializationScript);
23+
await context.GrantPermissionsAsync(UiTestHostConstants.GrantedPermissions, new BrowserContextGrantPermissionsOptions
24+
{
25+
Origin = baseAddress
26+
});
27+
await ConfigureMediaHarnessAsync(context);
28+
}
29+
30+
private static Task<IBrowserContext> CreateBrowserContextAsync(IBrowser browser, string baseAddress) =>
31+
browser.NewContextAsync(CreateBrowserContextOptions(baseAddress));
32+
33+
private static BrowserNewContextOptions CreateBrowserContextOptions(string baseAddress) => new()
34+
{
35+
BaseURL = baseAddress,
36+
ViewportSize = new()
37+
{
38+
Width = BrowserTestConstants.Viewport.DefaultWidth,
39+
Height = BrowserTestConstants.Viewport.DefaultHeight
40+
}
41+
};
42+
43+
private static async Task PrimeIsolatedBrowserStorageAsync(IPage page, string baseAddress)
44+
{
45+
await page.GotoAsync($"{baseAddress}{UiTestHostConstants.BlankPagePath}");
46+
await page.EvaluateAsync(
47+
UiTestHostConstants.ResetBrowserStorageScript,
48+
UiTestHostConstants.BrowserStorageDatabaseName);
49+
await page.EvaluateAsync(BrowserTestLibrarySeedData.CreateInitializationScript());
50+
}
51+
52+
private static async Task WarmUpRuntimeAsync(IBrowser browser, string baseAddress)
53+
{
54+
var context = await CreateBrowserContextAsync(browser, baseAddress);
55+
await InitializeContextAsync(context, baseAddress);
56+
57+
var page = await context.NewPageAsync();
58+
var browserErrors = BrowserErrorCollector.Attach(page);
59+
60+
try
61+
{
62+
PreparePage(page);
63+
await PrimeIsolatedBrowserStorageAsync(page, baseAddress);
64+
65+
foreach (var (route, pageTestId) in RuntimeWarmupRoutes)
66+
{
67+
await WarmUpRouteAsync(page, route, pageTestId);
68+
}
69+
70+
await browserErrors.AssertNoCriticalUiErrorsAsync();
71+
}
72+
catch (Exception exception)
73+
{
74+
throw BuildWarmupFailure(exception, browserErrors.Describe());
75+
}
76+
finally
77+
{
78+
await DisposeContextAsync(context);
79+
}
80+
}
81+
82+
private static async Task WarmUpRouteAsync(IPage page, string route, string pageTestId)
83+
{
84+
await page.GotoAsync(route);
85+
await Expect(page.GetByTestId(pageTestId))
86+
.ToBeVisibleAsync(new() { Timeout = BrowserTestConstants.Timing.RuntimeWarmupVisibleTimeoutMs });
87+
}
88+
89+
private static InvalidOperationException BuildWarmupFailure(Exception exception, string browserDiagnostics) =>
90+
new(
91+
"Shared UI test runtime warmup failed." + Environment.NewLine +
92+
"Captured browser errors:" + Environment.NewLine +
93+
browserDiagnostics + Environment.NewLine +
94+
exception,
95+
exception);
96+
97+
private static async Task DisposeContextAsync(IBrowserContext context)
98+
{
99+
try
100+
{
101+
await context.DisposeAsync();
102+
}
103+
catch
104+
{
105+
}
106+
}
107+
}

tests/PrompterOne.Web.UITests/Infrastructure/StandaloneAppFixture.cs

Lines changed: 6 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,7 @@ private async Task<IBrowserContext> CreateTrackedContextAsync()
139139
{
140140
var runtime = await EnsureRuntimeHandleAsync();
141141
var context = await CreateBrowserContextAsync(runtime.Browser);
142-
await context.AddInitScriptAsync(BrowserTestLibrarySeedData.CreateInitializationScript());
143-
await context.AddInitScriptAsync(UiTestHostConstants.RuntimeTelemetryHarnessInitializationScript);
144-
await context.GrantPermissionsAsync(UiTestHostConstants.GrantedPermissions, new BrowserContextGrantPermissionsOptions
145-
{
146-
Origin = runtime.BaseAddress
147-
});
148-
await ConfigureMediaHarnessAsync(context);
142+
await InitializeContextAsync(context, BaseAddress);
149143
EnsureContextTracked(context);
150144
return context;
151145
}
@@ -210,43 +204,21 @@ private async Task<IBrowserContext> CreateBrowserContextAsync(IBrowser browser)
210204
{
211205
try
212206
{
213-
return await browser.NewContextAsync(new BrowserNewContextOptions
214-
{
215-
BaseURL = BaseAddress,
216-
ViewportSize = new()
217-
{
218-
Width = BrowserTestConstants.Viewport.DefaultWidth,
219-
Height = BrowserTestConstants.Viewport.DefaultHeight
220-
}
221-
});
207+
return await CreateBrowserContextAsync(browser, BaseAddress);
222208
}
223209
catch (PlaywrightException exception) when (IsBrowserClosedException(exception))
224210
{
225211
_runtimeHandle = await SharedRuntime.AcquireAsync();
226-
return await _runtimeHandle.Browser.NewContextAsync(new BrowserNewContextOptions
227-
{
228-
BaseURL = _runtimeHandle.BaseAddress,
229-
ViewportSize = new()
230-
{
231-
Width = BrowserTestConstants.Viewport.DefaultWidth,
232-
Height = BrowserTestConstants.Viewport.DefaultHeight
233-
}
234-
});
212+
return await CreateBrowserContextAsync(_runtimeHandle.Browser, _runtimeHandle.BaseAddress);
235213
}
236214
}
237215

238216
private static bool IsBrowserClosedException(PlaywrightException exception) =>
239217
exception.Message.Contains("Target page, context or browser has been closed", StringComparison.Ordinal)
240218
|| exception.Message.Contains("Process exited", StringComparison.Ordinal);
241219

242-
private async Task PrimeIsolatedBrowserStorageAsync(IPage page)
243-
{
244-
await page.GotoAsync($"{BaseAddress}{UiTestHostConstants.BlankPagePath}");
245-
await page.EvaluateAsync(
246-
UiTestHostConstants.ResetBrowserStorageScript,
247-
UiTestHostConstants.BrowserStorageDatabaseName);
248-
await page.EvaluateAsync(BrowserTestLibrarySeedData.CreateInitializationScript());
249-
}
220+
private Task PrimeIsolatedBrowserStorageAsync(IPage page) =>
221+
PrimeIsolatedBrowserStorageAsync(page, BaseAddress);
250222

251223
private static void PreparePage(IPage page)
252224
{
@@ -311,6 +283,7 @@ private static async Task StartRuntimeAsync()
311283
_playwright = await Microsoft.Playwright.Playwright.CreateAsync();
312284
_playwright.Selectors.SetTestIdAttribute(BrowserTestConstants.Html.DataTestAttribute);
313285
_browser = await CreateBrowserAsync(_playwright);
286+
await WarmUpRuntimeAsync(_browser, _server.BaseAddress);
314287
}
315288

316289
private static StaticSpaServer CreateServer()

tests/PrompterOne.Web.UITests/Support/BrowserTestConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,7 @@ public static class Timing
872872
public const int DefaultVisibleTimeoutMs = 10_000;
873873
public const int DiagnosticPollDelayMs = 50;
874874
public const int ExtendedVisibleTimeoutMs = 15_000;
875+
public const int RuntimeWarmupVisibleTimeoutMs = 45_000;
875876
public const int NewDraftPersistGraceDelayMs = 700;
876877
public const int NewDraftPersistSettleDelayMs = 2_200;
877878
public const int FloatingToolbarSettleDelayMs = 500;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using Microsoft.Playwright;
2+
using PrompterOne.Shared.Contracts;
3+
using static Microsoft.Playwright.Assertions;
4+
5+
namespace PrompterOne.Web.UITests;
6+
7+
internal static class EditorTooltipDriver
8+
{
9+
internal static ILocator GetToolbarTooltip(IPage page, string tooltipText) =>
10+
page.GetByTestId(UiTestIds.Editor.ToolbarTooltip)
11+
.Filter(new() { HasTextString = tooltipText });
12+
13+
internal static async Task WaitUntilFullyVisibleAsync(IPage page, ILocator tooltip, string tooltipText)
14+
{
15+
await Expect(tooltip).ToBeVisibleAsync();
16+
17+
await page.WaitForFunctionAsync(
18+
"""
19+
args => Array.from(document.querySelectorAll(`[data-test="${args.testId}"]`))
20+
.filter(node => (node.textContent ?? '').includes(args.text))
21+
.some(node => {
22+
const styles = getComputedStyle(node);
23+
return styles.visibility !== "hidden"
24+
&& Number.parseFloat(styles.opacity || "0") >= args.minimumOpacity;
25+
})
26+
""",
27+
new
28+
{
29+
minimumOpacity = BrowserTestConstants.EditorFlow.MinimumVisibleTooltipOpacity,
30+
testId = UiTestIds.Editor.ToolbarTooltip,
31+
text = tooltipText
32+
},
33+
new() { Timeout = BrowserTestConstants.Timing.DefaultVisibleTimeoutMs });
34+
35+
await Expect(tooltip).ToHaveTextAsync(tooltipText);
36+
}
37+
38+
internal static async Task<double> ReadOpacityAsync(ILocator locator) =>
39+
await locator.EvaluateAsync<double>(
40+
"""
41+
element => Number.parseFloat(getComputedStyle(element).opacity)
42+
""");
43+
}

0 commit comments

Comments
 (0)