Skip to content

Commit 0bfaa68

Browse files
committed
Refresh performance metrics in assertions
Add Page.RefreshPerformanceMetricsAsync to re-read window.__motusPerf via Runtime.evaluate and merge values into LastPerformanceMetrics (uses System.Text.Json and a ReadPerfScript). Call this refresh from all page performance assertions (LCP, FCP, TTFB, CLS, INP) so retries can pick up metrics that arrive after initial collection. Update sample tests to use Fixtures.SetPageContentAsync(SimplePage) instead of navigating to example.com and remove the Integration TestCategory from these tests.
1 parent 9a1cf65 commit 0bfaa68

3 files changed

Lines changed: 70 additions & 7 deletions

File tree

samples/Motus.Samples/Tests/PerformanceTests.cs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,27 +40,23 @@ public async Task MethodAttribute_OverridesClassBudget()
4040
}
4141

4242
[TestMethod]
43-
[TestCategory("Integration")]
4443
public async Task LcpBelow_IndividualMetricAssertion()
4544
{
46-
// Individual metric assertions require a real HTTP origin for Web Vitals to fire.
47-
await Page.GotoAsync("https://example.com");
45+
await Fixtures.SetPageContentAsync(Page, SimplePage);
4846
await Expect.That(Page).ToHaveLcpBelowAsync(5000);
4947
}
5048

5149
[TestMethod]
52-
[TestCategory("Integration")]
5350
public async Task FcpBelow_IndividualMetricAssertion()
5451
{
55-
await Page.GotoAsync("https://example.com");
52+
await Fixtures.SetPageContentAsync(Page, SimplePage);
5653
await Expect.That(Page).ToHaveFcpBelowAsync(5000);
5754
}
5855

5956
[TestMethod]
60-
[TestCategory("Integration")]
6157
public async Task ClsBelow_NoLayoutShift()
6258
{
63-
await Page.GotoAsync("https://example.com");
59+
await Fixtures.SetPageContentAsync(Page, SimplePage);
6460
await Expect.That(Page).ToHaveClsBelowAsync(0.5);
6561
}
6662

src/Motus/Assertions/PageAssertions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public async Task ToMeetPerformanceBudgetAsync(AssertionOptions? options = null)
8787

8888
await RetryAsync(async ct =>
8989
{
90+
await _page.RefreshPerformanceMetricsAsync().ConfigureAwait(false);
9091
var metrics = _page.LastPerformanceMetrics;
9192
if (metrics is null)
9293
return (false, "<no metrics collected>");
@@ -103,6 +104,7 @@ await RetryAsync(async ct =>
103104
public Task ToHaveLcpBelowAsync(double thresholdMs, AssertionOptions? options = null) =>
104105
RetryAsync(async ct =>
105106
{
107+
await _page.RefreshPerformanceMetricsAsync().ConfigureAwait(false);
106108
var metrics = _page.LastPerformanceMetrics;
107109
if (metrics?.Lcp is null)
108110
return (false, "<LCP not collected>");
@@ -113,6 +115,7 @@ public Task ToHaveLcpBelowAsync(double thresholdMs, AssertionOptions? options =
113115
public Task ToHaveFcpBelowAsync(double thresholdMs, AssertionOptions? options = null) =>
114116
RetryAsync(async ct =>
115117
{
118+
await _page.RefreshPerformanceMetricsAsync().ConfigureAwait(false);
116119
var metrics = _page.LastPerformanceMetrics;
117120
if (metrics?.Fcp is null)
118121
return (false, "<FCP not collected>");
@@ -123,6 +126,7 @@ public Task ToHaveFcpBelowAsync(double thresholdMs, AssertionOptions? options =
123126
public Task ToHaveTtfbBelowAsync(double thresholdMs, AssertionOptions? options = null) =>
124127
RetryAsync(async ct =>
125128
{
129+
await _page.RefreshPerformanceMetricsAsync().ConfigureAwait(false);
126130
var metrics = _page.LastPerformanceMetrics;
127131
if (metrics?.Ttfb is null)
128132
return (false, "<TTFB not collected>");
@@ -133,6 +137,7 @@ public Task ToHaveTtfbBelowAsync(double thresholdMs, AssertionOptions? options =
133137
public Task ToHaveClsBelowAsync(double threshold, AssertionOptions? options = null) =>
134138
RetryAsync(async ct =>
135139
{
140+
await _page.RefreshPerformanceMetricsAsync().ConfigureAwait(false);
136141
var metrics = _page.LastPerformanceMetrics;
137142
if (metrics?.Cls is null)
138143
return (false, "<CLS not collected>");
@@ -143,6 +148,7 @@ public Task ToHaveClsBelowAsync(double threshold, AssertionOptions? options = nu
143148
public Task ToHaveInpBelowAsync(double thresholdMs, AssertionOptions? options = null) =>
144149
RetryAsync(async ct =>
145150
{
151+
await _page.RefreshPerformanceMetricsAsync().ConfigureAwait(false);
146152
var metrics = _page.LastPerformanceMetrics;
147153
if (metrics?.Inp is null)
148154
return (false, "<INP not collected>");

src/Motus/Page/Page.Performance.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
using System.Text.Json;
12
using Motus.Abstractions;
23

34
namespace Motus;
45

56
internal sealed partial class Page
67
{
8+
private const string ReadPerfScript = """
9+
JSON.stringify(window.__motusPerf || { lcp: null, cls: 0, inp: null, layoutShifts: [], fcp: null })
10+
""";
11+
712
/// <summary>
813
/// The most recent performance metrics, set by <see cref="PerformanceMetricsCollector"/>
914
/// after navigation or page close. Null when the hook is disabled or no collection has run.
@@ -15,4 +20,60 @@ internal sealed partial class Page
1520
/// from <see cref="PerformanceBudgetAttribute"/> resolution.
1621
/// </summary>
1722
internal PerformanceBudget? ActivePerformanceBudget { get; set; }
23+
24+
/// <summary>
25+
/// Re-reads the PerformanceObserver data from the page and updates
26+
/// <see cref="LastPerformanceMetrics"/>. Called by assertions during retry
27+
/// to pick up metrics that arrived after the initial post-navigation collection.
28+
/// </summary>
29+
internal async Task RefreshPerformanceMetricsAsync()
30+
{
31+
try
32+
{
33+
var evalResult = await _session.SendAsync(
34+
"Runtime.evaluate",
35+
new RuntimeEvaluateParams(ReadPerfScript, ReturnByValue: true),
36+
CdpJsonContext.Default.RuntimeEvaluateParams,
37+
CdpJsonContext.Default.RuntimeEvaluateResult,
38+
CancellationToken.None).ConfigureAwait(false);
39+
40+
if (evalResult.ExceptionDetails is not null || evalResult.Result.Value is not { } jsonValue)
41+
return;
42+
43+
var json = jsonValue.GetString();
44+
if (json is null)
45+
return;
46+
47+
using var doc = JsonDocument.Parse(json);
48+
var root = doc.RootElement;
49+
50+
var existing = LastPerformanceMetrics;
51+
52+
double? lcp = existing?.Lcp, fcp = existing?.Fcp, cls = existing?.Cls, inp = existing?.Inp;
53+
54+
if (root.TryGetProperty("lcp", out var lcpEl) && lcpEl.ValueKind == JsonValueKind.Number)
55+
lcp = lcpEl.GetDouble();
56+
if (root.TryGetProperty("fcp", out var fcpEl) && fcpEl.ValueKind == JsonValueKind.Number)
57+
fcp = fcpEl.GetDouble();
58+
if (root.TryGetProperty("cls", out var clsEl) && clsEl.ValueKind == JsonValueKind.Number)
59+
cls = clsEl.GetDouble();
60+
if (root.TryGetProperty("inp", out var inpEl) && inpEl.ValueKind == JsonValueKind.Number)
61+
inp = inpEl.GetDouble();
62+
63+
LastPerformanceMetrics = new PerformanceMetrics(
64+
Lcp: lcp,
65+
Fcp: fcp,
66+
Ttfb: existing?.Ttfb,
67+
Cls: cls,
68+
Inp: inp,
69+
JsHeapSize: existing?.JsHeapSize,
70+
DomNodeCount: existing?.DomNodeCount,
71+
LayoutShifts: existing?.LayoutShifts ?? [],
72+
CollectedAtUtc: DateTime.UtcNow);
73+
}
74+
catch
75+
{
76+
// Best effort; keep existing metrics
77+
}
78+
}
1879
}

0 commit comments

Comments
 (0)