Skip to content

Commit 721befa

Browse files
committed
Stabilize editor browser suite checks
1 parent 00da3fb commit 721befa

8 files changed

Lines changed: 188 additions & 26 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ private static async Task<DatePickerMetrics> OpenEditorAndReadDatePickerMetricsA
3232
{
3333
await page.GotoAsync(
3434
BrowserTestConstants.Routes.EditorDemo,
35-
new() { WaitUntil = WaitUntilState.NetworkIdle });
35+
new() { WaitUntil = WaitUntilState.DOMContentLoaded });
3636

3737
var createdInput = page.GetByTestId(UiTestIds.Editor.Created);
3838
await Expect(createdInput).ToBeVisibleAsync(
@@ -62,7 +62,7 @@ private static async Task SwitchThemeAsync(IPage page, string theme)
6262
{
6363
await page.GotoAsync(
6464
BrowserTestConstants.Routes.Settings,
65-
new() { WaitUntil = WaitUntilState.NetworkIdle });
65+
new() { WaitUntil = WaitUntilState.DOMContentLoaded });
6666
await Expect(page.GetByTestId(UiTestIds.Settings.Page)).ToBeVisibleAsync(
6767
new() { Timeout = BrowserTestConstants.Timing.ExtendedVisibleTimeoutMs });
6868
await page.GetByTestId(UiTestIds.Settings.NavAppearance).ClickAsync();

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

Lines changed: 156 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,24 +88,90 @@ await page.EvaluateAsync(
8888
8989
const samples = [];
9090
const longTasks = [];
91+
const errors = [];
92+
const pendingSamples = [];
9193
const observer = new PerformanceObserver(list => {
9294
for (const entry of list.getEntries()) {
93-
longTasks.push(entry.duration);
95+
longTasks.push({
96+
duration: entry.duration,
97+
endTime: entry.startTime + entry.duration,
98+
startTime: entry.startTime
99+
});
94100
}
95101
});
96102
97103
observer.observe({ type: "longtask" });
98-
input.addEventListener(args.proxyChangedEventName, () => {
99-
const started = performance.now();
100-
requestAnimationFrame(() => {
104+
105+
const probe = {
106+
captureStart: performance.now(),
107+
errors,
108+
eventCount: 0,
109+
lastExpectedLength: -1,
110+
longTasks,
111+
observer,
112+
overlayObserver: null,
113+
pendingSampleCount: 0,
114+
pendingSamples,
115+
samples
116+
};
117+
118+
const readRenderedLength = () =>
119+
Number.parseInt(overlay.dataset.renderedLength ?? "-1", 10);
120+
121+
const completeVisibleSamples = renderedLength => {
122+
while (pendingSamples.length > 0 && renderedLength >= pendingSamples[0].expectedLength) {
123+
const pendingSample = pendingSamples.shift();
124+
const completedAt = performance.now();
101125
samples.push({
102-
latency: performance.now() - started,
103-
renderedLength: Number.parseInt(overlay.dataset.renderedLength ?? "-1", 10)
126+
completedAt,
127+
latency: completedAt - pendingSample.started,
128+
renderedLength,
129+
started: pendingSample.started
104130
});
131+
}
132+
133+
probe.pendingSampleCount = pendingSamples.length;
134+
};
135+
136+
const overlayObserver = new MutationObserver(() => {
137+
try {
138+
completeVisibleSamples(readRenderedLength());
139+
}
140+
catch (error) {
141+
errors.push(String(error?.stack ?? error));
142+
}
143+
});
144+
145+
overlayObserver.observe(overlay, {
146+
attributes: true,
147+
childList: true,
148+
characterData: true,
149+
subtree: true
150+
});
151+
152+
probe.overlayObserver = overlayObserver;
153+
154+
input.addEventListener(args.proxyChangedEventName, event => {
155+
probe.eventCount += 1;
156+
const expectedLength = Number.isFinite(event?.detail?.textLength)
157+
? event.detail.textLength
158+
: input.value.length;
159+
probe.lastExpectedLength = expectedLength;
160+
pendingSamples.push({
161+
expectedLength,
162+
started: performance.now()
105163
});
164+
probe.pendingSampleCount = pendingSamples.length;
165+
166+
try {
167+
completeVisibleSamples(readRenderedLength());
168+
}
169+
catch (error) {
170+
errors.push(String(error?.stack ?? error));
171+
}
106172
}, { passive: true });
107173
108-
window.__editorHugeDraftProbe = { longTasks, observer, samples };
174+
window.__editorHugeDraftProbe = probe;
109175
}
110176
""",
111177
new
@@ -125,12 +191,39 @@ await page.EvaluateAsync(
125191
throw new Error("Huge draft performance probe was not initialized before reset.");
126192
}
127193
194+
probe.captureStart = performance.now();
195+
probe.errors.length = 0;
196+
probe.eventCount = 0;
197+
probe.lastExpectedLength = -1;
128198
probe.longTasks.length = 0;
199+
probe.pendingSampleCount = 0;
200+
probe.pendingSamples.length = 0;
129201
probe.samples.length = 0;
130202
}
131203
""");
132204
await page.Keyboard.TypeAsync(EditorLargeDraftPerformanceTestData.FollowupTypingText, new() { Delay = 0 });
133-
await page.WaitForTimeoutAsync(EditorLargeDraftPerformanceTestData.ObservationDelayMs);
205+
await page.WaitForFunctionAsync(
206+
"""
207+
(args) => {
208+
const overlay = document.querySelector(`[data-test="${args.overlayTestId}"]`);
209+
const probe = window.__editorHugeDraftProbe;
210+
if (!overlay || !probe) {
211+
return false;
212+
}
213+
214+
const renderedLength = Number.parseInt(overlay.dataset.renderedLength ?? "-1", 10);
215+
return renderedLength >= args.expectedLength &&
216+
probe.pendingSampleCount === 0 &&
217+
probe.samples.length >= args.expectedSampleCount;
218+
}
219+
""",
220+
new
221+
{
222+
expectedLength,
223+
expectedSampleCount = EditorLargeDraftPerformanceTestData.FollowupTypingText.Length,
224+
overlayTestId = UiTestIds.Editor.SourceHighlight
225+
},
226+
new() { Timeout = EditorLargeDraftPerformanceTestData.ObservationDelayMs });
134227

135228
var result = await page.EvaluateAsync<HugeDraftProbeResult>(
136229
"""
@@ -143,12 +236,47 @@ await page.EvaluateAsync(
143236
}
144237
145238
probe.observer.disconnect();
239+
probe.overlayObserver?.disconnect?.();
240+
241+
const samples = probe.samples ?? [];
242+
const captureStart = Number.isFinite(probe.captureStart)
243+
? probe.captureStart
244+
: 0;
245+
const responseWindowStart = samples.length
246+
? Math.min(...samples.map(sample => sample.started))
247+
: -1;
248+
const responseWindowEnd = samples.length
249+
? Math.max(...samples.map(sample => sample.completedAt))
250+
: -1;
251+
const relevantWindowStart = responseWindowStart >= 0
252+
? Math.max(captureStart, responseWindowStart)
253+
: captureStart;
254+
const observedLongTasks = (probe.longTasks ?? [])
255+
.filter(entry => entry.endTime >= captureStart);
256+
const responseWindowLongTasks = responseWindowEnd >= 0
257+
? observedLongTasks.filter(entry =>
258+
entry.endTime >= relevantWindowStart &&
259+
entry.startTime <= responseWindowEnd)
260+
: [];
261+
146262
return {
147263
finalInputLength: input.value.length,
148264
finalRenderedLength: Number.parseInt(overlay.dataset.renderedLength ?? "-1", 10),
149-
followupMaxLongTaskMs: probe.longTasks.length ? Math.max(...probe.longTasks) : 0,
150-
typingLatencyMs: probe.samples[probe.samples.length - 1]?.latency ?? -1,
151-
typingSampleCount: probe.samples.length
265+
eventCount: probe.eventCount ?? -1,
266+
errors: probe.errors ?? [],
267+
followupMaxLongTaskMs: responseWindowLongTasks.length
268+
? Math.max(...responseWindowLongTasks.map(entry => entry.duration))
269+
: 0,
270+
lastExpectedLength: probe.lastExpectedLength ?? -1,
271+
maxObservedLongTaskMs: observedLongTasks.length
272+
? Math.max(...observedLongTasks.map(entry => entry.duration))
273+
: 0,
274+
pendingSampleCount: probe.pendingSampleCount ?? -1,
275+
responseWindowDurationMs: responseWindowEnd >= 0
276+
? Math.max(0, responseWindowEnd - relevantWindowStart)
277+
: -1,
278+
typingLatencyMs: samples[samples.length - 1]?.latency ?? -1,
279+
typingSampleCount: samples.length
152280
};
153281
}
154282
""",
@@ -160,11 +288,13 @@ await page.EvaluateAsync(
160288

161289
await Assert.That(result.FinalInputLength).IsEqualTo(expectedLength);
162290
await Assert.That(result.FinalRenderedLength).IsEqualTo(expectedLength);
163-
await Assert.That(result.TypingSampleCount >= 2).IsTrue();
291+
await Assert.That(result.EventCount >= EditorLargeDraftPerformanceTestData.FollowupTypingText.Length).IsTrue().Because($"Expected Monaco proxy change events for the huge draft follow-up typing, but observed {result.EventCount}. Pending samples: {result.PendingSampleCount}; LastExpectedLength: {result.LastExpectedLength}; FinalInputLength: {result.FinalInputLength}; FinalRenderedLength: {result.FinalRenderedLength}.");
292+
await Assert.That(result.TypingSampleCount >= EditorLargeDraftPerformanceTestData.FollowupTypingText.Length).IsTrue().Because($"Expected the huge draft follow-up typing probe to capture visible overlay samples for each character, but observed {result.TypingSampleCount} sample(s) after {result.EventCount} proxy change event(s). Pending samples: {result.PendingSampleCount}; LastExpectedLength: {result.LastExpectedLength}; Errors: {string.Join(" || ", result.Errors)}.");
293+
await Assert.That(result.PendingSampleCount).IsEqualTo(0);
164294
await Assert.That(result.FollowupMaxLongTaskMs >= 0 &&
165-
result.FollowupMaxLongTaskMs <= EditorLargeDraftPerformanceTestData.MaxHugeFollowupLongTaskMs).IsTrue().Because($"Huge draft follow-up long task exceeded the acceptance budget. FollowupMaxLongTaskMs: {result.FollowupMaxLongTaskMs}; MaxHugeFollowupLongTaskMs: {EditorLargeDraftPerformanceTestData.MaxHugeFollowupLongTaskMs}; TypingLatencyMs: {result.TypingLatencyMs}; TypingSampleCount: {result.TypingSampleCount}; FinalInputLength: {result.FinalInputLength}; FinalRenderedLength: {result.FinalRenderedLength}.");
295+
result.FollowupMaxLongTaskMs <= EditorLargeDraftPerformanceTestData.MaxHugeFollowupLongTaskMs).IsTrue().Because($"Huge draft follow-up long task exceeded the acceptance budget. FollowupMaxLongTaskMs: {result.FollowupMaxLongTaskMs}; MaxHugeFollowupLongTaskMs: {EditorLargeDraftPerformanceTestData.MaxHugeFollowupLongTaskMs}; MaxObservedLongTaskMs: {result.MaxObservedLongTaskMs}; ResponseWindowDurationMs: {result.ResponseWindowDurationMs}; TypingLatencyMs: {result.TypingLatencyMs}; TypingSampleCount: {result.TypingSampleCount}; EventCount: {result.EventCount}; PendingSampleCount: {result.PendingSampleCount}; FinalInputLength: {result.FinalInputLength}; FinalRenderedLength: {result.FinalRenderedLength}; Errors: {string.Join(" || ", result.Errors)}.");
166296
await Assert.That(result.TypingLatencyMs >= 0 &&
167-
result.TypingLatencyMs <= EditorLargeDraftPerformanceTestData.MaxHugeTypingLatencyMs).IsTrue().Because($"Huge draft typing latency exceeded the acceptance budget. TypingLatencyMs: {result.TypingLatencyMs}; MaxHugeTypingLatencyMs: {EditorLargeDraftPerformanceTestData.MaxHugeTypingLatencyMs}; FollowupMaxLongTaskMs: {result.FollowupMaxLongTaskMs}; TypingSampleCount: {result.TypingSampleCount}; FinalInputLength: {result.FinalInputLength}; FinalRenderedLength: {result.FinalRenderedLength}.");
297+
result.TypingLatencyMs <= EditorLargeDraftPerformanceTestData.MaxHugeTypingLatencyMs).IsTrue().Because($"Huge draft typing latency exceeded the acceptance budget. TypingLatencyMs: {result.TypingLatencyMs}; MaxHugeTypingLatencyMs: {EditorLargeDraftPerformanceTestData.MaxHugeTypingLatencyMs}; FollowupMaxLongTaskMs: {result.FollowupMaxLongTaskMs}; MaxObservedLongTaskMs: {result.MaxObservedLongTaskMs}; ResponseWindowDurationMs: {result.ResponseWindowDurationMs}; TypingSampleCount: {result.TypingSampleCount}; EventCount: {result.EventCount}; PendingSampleCount: {result.PendingSampleCount}; FinalInputLength: {result.FinalInputLength}; FinalRenderedLength: {result.FinalRenderedLength}; Errors: {string.Join(" || ", result.Errors)}.");
168298
}
169299
finally
170300
{
@@ -174,12 +304,24 @@ await Assert.That(result.TypingLatencyMs >= 0 &&
174304

175305
private sealed class HugeDraftProbeResult
176306
{
307+
public int EventCount { get; set; }
308+
309+
public string[] Errors { get; set; } = [];
310+
177311
public int FinalInputLength { get; set; }
178312

179313
public int FinalRenderedLength { get; set; }
180314

181315
public double FollowupMaxLongTaskMs { get; set; }
182316

317+
public int LastExpectedLength { get; set; }
318+
319+
public double MaxObservedLongTaskMs { get; set; }
320+
321+
public int PendingSampleCount { get; set; }
322+
323+
public double ResponseWindowDurationMs { get; set; }
324+
183325
public double TypingLatencyMs { get; set; }
184326

185327
public int TypingSampleCount { get; set; }

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@ await EditorMonacoDriver.WaitForSelectionScrollAsync(
8282
}
8383
""");
8484

85-
await Assert.That(stageState.ScrollTop > 0).IsTrue();
85+
await Assert.That(stageState.VisibleRange).IsNotNull();
86+
await Assert.That(
87+
stageState.VisibleRange!.StartLineNumber > 1 &&
88+
stageState.Selection.Line >= stageState.VisibleRange.StartLineNumber &&
89+
stageState.Selection.Line <= stageState.VisibleRange.EndLineNumber).IsTrue().Because($"Expected Monaco to own the vertical scroll surface after centering the selection line, but the visible range did not move off the first line or did not contain the caret line. ScrollTop={stageState.ScrollTop}; SelectionLine={stageState.Selection.Line}; VisibleStart={stageState.VisibleRange.StartLineNumber}; VisibleEnd={stageState.VisibleRange.EndLineNumber}; LineCount={stageState.LineCount}.");
8690
await Assert.That(scrollState.HostScrollTop).IsEqualTo(BrowserTestConstants.Editor.MaxSourceScrollHostTopPx);
8791
await Assert.That(
8892
string.Equals(scrollState.HostOverflow, BrowserTestConstants.Editor.HiddenOverflowValue, StringComparison.Ordinal) ||

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public Task EditorScreen_LightTheme_UsesReadableMainPanelsAndMonacoSurface() =>
2727
await SwitchThemeAsync(page);
2828
await page.GotoAsync(
2929
BrowserTestConstants.Routes.EditorDemo,
30-
new() { WaitUntil = WaitUntilState.NetworkIdle });
30+
new() { WaitUntil = WaitUntilState.DOMContentLoaded });
3131
await Expect(page.GetByTestId(UiTestIds.Editor.Page)).ToBeVisibleAsync(
3232
new() { Timeout = BrowserTestConstants.Timing.ExtendedVisibleTimeoutMs });
3333
await Expect(page.Locator("html")).ToHaveAttributeAsync(
@@ -70,7 +70,7 @@ private static async Task SwitchThemeAsync(IPage page)
7070
{
7171
await page.GotoAsync(
7272
BrowserTestConstants.Routes.Settings,
73-
new() { WaitUntil = WaitUntilState.NetworkIdle });
73+
new() { WaitUntil = WaitUntilState.DOMContentLoaded });
7474
await Expect(page.GetByTestId(UiTestIds.Settings.Page)).ToBeVisibleAsync(
7575
new() { Timeout = BrowserTestConstants.Timing.ExtendedVisibleTimeoutMs });
7676
await page.GetByTestId(UiTestIds.Settings.NavAppearance).ClickAsync();

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public Task EditorScreen_LightTheme_EmotionMenu_UsesReadableDropdownAndCustomToo
2424
{
2525
await page.GotoAsync(
2626
BrowserTestConstants.Routes.Settings,
27-
new() { WaitUntil = WaitUntilState.NetworkIdle });
27+
new() { WaitUntil = WaitUntilState.DOMContentLoaded });
2828
await Expect(page.GetByTestId(UiTestIds.Settings.Page)).ToBeVisibleAsync(
2929
new() { Timeout = BrowserTestConstants.Timing.ExtendedVisibleTimeoutMs });
3030

@@ -37,7 +37,7 @@ await Expect(page.Locator("html")).ToHaveAttributeAsync(
3737

3838
await page.GotoAsync(
3939
BrowserTestConstants.Routes.EditorDemo,
40-
new() { WaitUntil = WaitUntilState.NetworkIdle });
40+
new() { WaitUntil = WaitUntilState.DOMContentLoaded });
4141
await Expect(page.GetByTestId(UiTestIds.Editor.Page)).ToBeVisibleAsync(
4242
new() { Timeout = BrowserTestConstants.Timing.ExtendedVisibleTimeoutMs });
4343
await EditorMonacoDriver.WaitUntilReadyAsync(page);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ private static async Task OpenEditorAsync(IPage page)
222222
{
223223
await page.GotoAsync(
224224
BrowserTestConstants.Routes.EditorDemo,
225-
new() { WaitUntil = WaitUntilState.NetworkIdle });
225+
new() { WaitUntil = WaitUntilState.DOMContentLoaded });
226226
await Expect(page.GetByTestId(UiTestIds.Editor.Page))
227227
.ToBeVisibleAsync(new() { Timeout = BrowserTestConstants.Timing.DefaultVisibleTimeoutMs });
228228
await EditorMonacoDriver.WaitUntilReadyAsync(page);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public Task EditorToolbar_DropdownMenu_RendersAboveEditorSurface_AndReceivesPoin
3131

3232
await page.GotoAsync(
3333
BrowserTestConstants.Routes.EditorDemo,
34-
new() { WaitUntil = WaitUntilState.NetworkIdle });
34+
new() { WaitUntil = WaitUntilState.DOMContentLoaded });
3535
await Expect(page.GetByTestId(UiTestIds.Editor.Page))
3636
.ToBeVisibleAsync(new() { Timeout = BrowserTestConstants.Timing.DefaultVisibleTimeoutMs });
3737
await EditorMonacoDriver.WaitUntilReadyAsync(page);
@@ -48,7 +48,7 @@ public Task EditorToolbar_FloatingDropdownMenu_RendersAboveEditorSurface_AndRece
4848

4949
await page.GotoAsync(
5050
BrowserTestConstants.Routes.EditorDemo,
51-
new() { WaitUntil = WaitUntilState.NetworkIdle });
51+
new() { WaitUntil = WaitUntilState.DOMContentLoaded });
5252
await Expect(page.GetByTestId(UiTestIds.Editor.Page))
5353
.ToBeVisibleAsync(new() { Timeout = BrowserTestConstants.Timing.DefaultVisibleTimeoutMs });
5454
await EditorMonacoDriver.WaitUntilReadyAsync(page);

0 commit comments

Comments
 (0)