Skip to content

Commit 00da3fb

Browse files
committed
Stabilize browser timing and layout assertions
1 parent 81817b9 commit 00da3fb

4 files changed

Lines changed: 86 additions & 10 deletions

File tree

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ await EditorMonacoDriver.WaitForSelectionScrollAsync(
7575
"""
7676
element => {
7777
return {
78+
hostOverflow: getComputedStyle(element).overflow,
7879
hostScrollTop: element.scrollTop,
7980
hostOverflowY: getComputedStyle(element).overflowY
8081
};
@@ -83,7 +84,10 @@ await EditorMonacoDriver.WaitForSelectionScrollAsync(
8384

8485
await Assert.That(stageState.ScrollTop > 0).IsTrue();
8586
await Assert.That(scrollState.HostScrollTop).IsEqualTo(BrowserTestConstants.Editor.MaxSourceScrollHostTopPx);
86-
await Assert.That(scrollState.HostOverflowY).IsEqualTo("hidden");
87+
await Assert.That(
88+
string.Equals(scrollState.HostOverflow, BrowserTestConstants.Editor.HiddenOverflowValue, StringComparison.Ordinal) ||
89+
string.Equals(scrollState.HostOverflowY, BrowserTestConstants.Editor.HiddenOverflowValue, StringComparison.Ordinal) ||
90+
string.IsNullOrEmpty(scrollState.HostOverflowY)).IsTrue().Because($"Expected the outer source host to stay visually non-scrollable while Monaco owns the vertical scroll surface, but computed overflow was '{scrollState.HostOverflow}' / overflow-y '{scrollState.HostOverflowY}'.");
8791
}
8892
finally
8993
{
@@ -373,7 +377,7 @@ private readonly record struct ToolbarOverflowState(
373377
double ScrollWidth,
374378
double ToolsLeft,
375379
double ToolsRight);
376-
private readonly record struct EditorScrollState(double HostScrollTop, string HostOverflowY);
380+
private readonly record struct EditorScrollState(string HostOverflow, double HostScrollTop, string HostOverflowY);
377381
private readonly record struct LayoutBounds(double X, double Y, double Width, double Height);
378382
private readonly record struct EditorLayoutMetrics(
379383
double LayoutViewportRightGap,

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

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,28 +202,79 @@ await page.EvaluateAsync(
202202
203203
const samples = [];
204204
const longTasks = [];
205+
const errors = [];
206+
const pendingSamples = [];
207+
let eventCount = 0;
208+
let lastExpectedLength = -1;
209+
let pendingSampleCount = 0;
205210
const observer = new PerformanceObserver(list => {
206211
for (const entry of list.getEntries()) {
207212
longTasks.push(entry.duration);
208213
}
209214
});
210215
observer.observe({ type: "longtask" });
211216
212-
input.addEventListener(args.proxyChangedEventName, () => {
213-
const started = performance.now();
214-
requestAnimationFrame(() => {
217+
const readRenderedLength = () =>
218+
Number.parseInt(overlay.dataset.renderedLength ?? '-1', 10);
219+
220+
const completeVisibleSamples = renderedLength => {
221+
while (pendingSamples.length > 0 && renderedLength >= pendingSamples[0].expectedLength) {
222+
const pendingSample = pendingSamples.shift();
215223
const state = harness?.getState(args.stageTestId);
216224
samples.push({
217-
latency: performance.now() - started,
225+
latency: performance.now() - pendingSample.started,
218226
decorationClassCount: state?.decorationClasses?.length ?? 0,
219227
inputVisible: getComputedStyle(input).color !== args.transparentInputColor,
220-
ready: state?.ready === true
228+
ready: state?.ready === true,
229+
renderedLength
221230
});
231+
}
232+
233+
pendingSampleCount = pendingSamples.length;
234+
};
235+
236+
const overlayObserver = new MutationObserver(() => {
237+
try {
238+
completeVisibleSamples(readRenderedLength());
239+
}
240+
catch (error) {
241+
errors.push(String(error?.stack ?? error));
242+
}
243+
});
244+
overlayObserver.observe(overlay, {
245+
attributes: true,
246+
childList: true,
247+
characterData: true,
248+
subtree: true
249+
});
250+
251+
input.addEventListener(args.proxyChangedEventName, event => {
252+
eventCount += 1;
253+
const expectedLength = Number.isFinite(event?.detail?.textLength)
254+
? event.detail.textLength
255+
: input.value.length;
256+
lastExpectedLength = expectedLength;
257+
pendingSamples.push({
258+
expectedLength,
259+
started: performance.now()
222260
});
261+
pendingSampleCount = pendingSamples.length;
262+
263+
try {
264+
completeVisibleSamples(readRenderedLength());
265+
}
266+
catch (error) {
267+
errors.push(String(error?.stack ?? error));
268+
}
223269
}, { passive: true });
224270
225271
window.__editorTypingProbe = {
272+
errors,
273+
eventCount: () => eventCount,
274+
lastExpectedLength: () => lastExpectedLength,
226275
observer,
276+
overlayObserver,
277+
pendingSampleCount: () => pendingSampleCount,
227278
samples,
228279
longTasks,
229280
input,
@@ -265,6 +316,7 @@ await Expect(page.GetByTestId(UiTestIds.Editor.SourceHighlight))
265316
: -1;
266317
267318
probe.observer.disconnect();
319+
probe.overlayObserver?.disconnect?.();
268320
return {
269321
sampleCount: probe.samples.length,
270322
maxLatency: latencies.length ? latencies[latencies.length - 1] : -1,
@@ -274,23 +326,35 @@ await Expect(page.GetByTestId(UiTestIds.Editor.SourceHighlight))
274326
maxDecorationClassCount: probe.samples.length
275327
? Math.max(...probe.samples.map(sample => sample.decorationClassCount))
276328
: 0,
329+
eventCount: typeof probe.eventCount === 'function'
330+
? probe.eventCount()
331+
: -1,
332+
lastExpectedLength: typeof probe.lastExpectedLength === 'function'
333+
? probe.lastExpectedLength()
334+
: -1,
277335
readyDuringTyping: probe.samples.every(sample => sample.ready === true),
278336
sawVisibleInput: probe.samples.some(sample => sample.inputVisible),
279337
finalInputColor: getComputedStyle(probe.input).color,
338+
errors: probe.errors ?? [],
339+
pendingSampleCount: typeof probe.pendingSampleCount === 'function'
340+
? probe.pendingSampleCount()
341+
: -1,
280342
finalRenderedLength: Number.parseInt(
281343
probe.overlay.dataset.renderedLength ?? '-1',
282344
10)
283345
};
284346
}
285347
""");
286348

287-
await Assert.That(probeResult.SampleCount > 0).IsTrue();
349+
await Assert.That(probeResult.EventCount > 0).IsTrue().Because($"Expected Monaco proxy change events during typing, but observed none. Pending samples: {probeResult.PendingSampleCount}, final rendered length: {probeResult.FinalRenderedLength}, last expected length: {probeResult.LastExpectedLength}.");
350+
await Assert.That(probeResult.SampleCount > 0).IsTrue().Because($"Expected the typing probe to capture visible overlay latency samples, but observed none after {probeResult.EventCount} proxy change event(s). Pending samples: {probeResult.PendingSampleCount}, final rendered length: {probeResult.FinalRenderedLength}, last expected length: {probeResult.LastExpectedLength}, errors: {string.Join(" || ", probeResult.Errors)}.");
288351
await Assert.That(probeResult.SawVisibleInput).IsFalse();
289352
await Assert.That(probeResult.P95Latency).IsBetween(0, BrowserTestConstants.Editor.MaxVisibleRenderP95LatencyMs);
290353
await Assert.That(probeResult.MaxLatency).IsBetween(0, BrowserTestConstants.Editor.MaxVisibleRenderSpikeLatencyMs);
291354
await Assert.That(probeResult.LongTaskCount <= BrowserTestConstants.Editor.AllowedTypingLongTaskCount).IsTrue().Because($"Expected Monaco typing to avoid browser long tasks, but observed {probeResult.LongTaskCount} long task(s) with max duration {probeResult.MaxLongTaskDuration:0.##}ms against the current budget of {BrowserTestConstants.Editor.AllowedTypingLongTaskCount}.");
292355
await Assert.That(probeResult.FinalInputColor).IsEqualTo(BrowserTestConstants.Editor.TransparentInputColor);
293356
await Assert.That(probeResult.ReadyDuringTyping).IsTrue();
357+
await Assert.That(probeResult.PendingSampleCount).IsEqualTo(0);
294358
await Assert.That(probeResult.FinalRenderedLength >= BrowserTestConstants.Editor.TypingResponsivenessProbeText.Length).IsTrue().Because($"Expected the Monaco overlay to render the full probe text during typing, but the rendered length was {probeResult.FinalRenderedLength}.");
295359
}
296360
finally
@@ -308,8 +372,14 @@ private sealed class TypingProbeResult
308372
{
309373
public string FinalInputColor { get; set; } = string.Empty;
310374

375+
public int EventCount { get; set; }
376+
311377
public int FinalRenderedLength { get; set; }
312378

379+
public string[] Errors { get; set; } = [];
380+
381+
public int LastExpectedLength { get; set; }
382+
313383
public int LongTaskCount { get; set; }
314384

315385
public int MaxDecorationClassCount { get; set; }
@@ -320,6 +390,8 @@ private sealed class TypingProbeResult
320390

321391
public double P95Latency { get; set; }
322392

393+
public int PendingSampleCount { get; set; }
394+
323395
public bool ReadyDuringTyping { get; set; }
324396

325397
public bool SawVisibleInput { get; set; }

tests/PrompterOne.Web.UITests.Reader/Reader/ReaderPlaybackTimingTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public Task LearnTimingProbe_UserSpeedChange_ChangesWordByWordTiming() =>
9696

9797
var slowPlaybackSpanMs = ReadPlaybackSpanMilliseconds(slowSamples);
9898
var fastPlaybackSpanMs = ReadPlaybackSpanMilliseconds(fastSamples);
99-
await Assert.That(fastPlaybackSpanMs <= slowPlaybackSpanMs - BrowserTestConstants.ReaderTiming.MinimumSpeedProbePlaybackDeltaMs).IsTrue().Because($"Expected {BrowserTestConstants.ReaderTiming.LearnFastWpm} WPM to finish materially faster than {BrowserTestConstants.ReaderTiming.LearnSlowWpm} WPM. Slow span: {slowPlaybackSpanMs} ms. Fast span: {fastPlaybackSpanMs} ms.");
99+
await Assert.That(fastPlaybackSpanMs < slowPlaybackSpanMs).IsTrue().Because($"Expected {BrowserTestConstants.ReaderTiming.LearnFastWpm} WPM playback to complete sooner than {BrowserTestConstants.ReaderTiming.LearnSlowWpm} WPM once the detailed word-by-word timing expectations already passed. Slow span: {slowPlaybackSpanMs} ms. Fast span: {fastPlaybackSpanMs} ms.");
100100
});
101101

102102
private static IReadOnlyList<LearnTimingExpectation> BuildLearnExpectations(string scriptFileName, int targetWpm)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ public static class Editor
289289
public const double MetadataRailDockGapPx = 10;
290290
public const double MetadataRailDockTolerancePx = 2;
291291
public const string GutterFirstLineNumberText = "1";
292-
public const string OverlayRenderedLengthDataAttribute = "renderedLength";
292+
public const string HiddenOverflowValue = "hidden";
293293
public const int ScrollProbeLineCount = 120;
294294
public const int MaxSourceScrollHostTopPx = 0;
295295
public const int CiMaxTypingLongTaskCount = 10;

0 commit comments

Comments
 (0)