diff --git a/src/PlaywrightNative.Tests/Library/Coverage/CoverageTests.cs b/src/PlaywrightNative.Tests/Library/Coverage/CoverageTests.cs
new file mode 100644
index 00000000..976bae5a
--- /dev/null
+++ b/src/PlaywrightNative.Tests/Library/Coverage/CoverageTests.cs
@@ -0,0 +1,443 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ * Modifications copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Microsoft.Playwright;
+using NUnit.Framework;
+using PlaywrightNative.Helpers;
+using PlaywrightNative.NUnit;
+
+namespace PlaywrightNative.Tests
+{
+ ///
+ /// Official library/coverage.spec.ts parity. The file-level
+ /// trace === 'on' skip does not apply here: tests never run with
+ /// an outer trace.
+ ///
+ [TestFixture]
+ [NonParallelizable]
+ public class CoverageTests : BrowserTestEx
+ {
+ private string _outputDir;
+
+ [SetUp]
+ public void CreateOutputDir()
+ {
+ _outputDir = Path.Combine(Path.GetTempPath(), "pw-coverage-" + Path.GetRandomFileName());
+ Directory.CreateDirectory(_outputDir);
+ }
+
+ [TearDown]
+ public void DeleteOutputDir()
+ {
+ try
+ {
+ Directory.Delete(_outputDir, recursive: true);
+ }
+ catch (IOException)
+ {
+ }
+ }
+
+ [PlaywrightTest("coverage.spec.ts", "should collect istanbul coverage into the trace")]
+ [Test, Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldCollectIstanbulCoverageIntoTheTrace()
+ {
+ IBrowserContext context = await Browser.NewContextAsync();
+ await context.Tracing.StartAsync(new TracingStartOptions { Coverage = true });
+
+ IPage page = await context.NewPageAsync();
+ await page.SetContentAsync(CoverageScript("a.js", 3));
+
+ // Coverage of a closed page is collected automatically.
+ IPage page2 = await context.NewPageAsync();
+ await page2.SetContentAsync(CoverageScript("b.js", 2));
+ await page2.CloseAsync();
+
+ string traceFile = OutputPath("trace.zip");
+ await context.Tracing.StopAsync(new() { Path = traceFile });
+ await context.CloseAsync();
+
+ Dictionary data = ParseTraceCoverage(traceFile);
+ Assert.That(data["a.js"].S["0"], Is.EqualTo(3));
+ Assert.That(data["b.js"].S["0"], Is.EqualTo(2));
+ }
+
+ [PlaywrightTest("coverage.spec.ts", "should collect coverage per trace chunk")]
+ [Test, Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldCollectCoveragePerTraceChunk()
+ {
+ IBrowserContext context = await Browser.NewContextAsync();
+ await context.Tracing.StartAsync(new TracingStartOptions { Coverage = true });
+ IPage page = await context.NewPageAsync();
+
+ await page.SetContentAsync(CoverageScript("a.js", 5));
+ string traceFile1 = OutputPath("trace1.zip");
+ await context.Tracing.StopChunkAsync(new() { Path = traceFile1 });
+
+ await context.Tracing.StartChunkAsync();
+ await page.SetContentAsync(CoverageScript("b.js", 7));
+ string traceFile2 = OutputPath("trace2.zip");
+ await context.Tracing.StopChunkAsync(new() { Path = traceFile2 });
+
+ await context.Tracing.StopAsync();
+ await context.CloseAsync();
+
+ Dictionary data1 = ParseTraceCoverage(traceFile1);
+ Assert.That(data1["a.js"].S["0"], Is.EqualTo(5));
+ Assert.That(data1.ContainsKey("b.js"), Is.False);
+
+ Dictionary data2 = ParseTraceCoverage(traceFile2);
+ Assert.That(data2["b.js"].S["0"], Is.EqualTo(7));
+ Assert.That(data2.ContainsKey("a.js"), Is.False);
+ }
+
+ [PlaywrightTest("coverage.spec.ts", "should report maps once and counters incrementally")]
+ [Test, Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldReportMapsOnceAndCountersIncrementally()
+ {
+ IBrowserContext context = await Browser.NewContextAsync();
+ await context.Tracing.StartAsync(new TracingStartOptions { Coverage = true });
+ IPage page = await context.NewPageAsync();
+ await page.GoToAsync(TestConstants.EmptyPage);
+
+ // Actions collect the counters on their own, so hit and collect in one evaluate.
+ IstanbulCoverageChunk[] first = ToChunks(await page.EvaluateAsync(
+ @"coverage => {
+ window.__coverage__ = JSON.parse(coverage);
+ return window.__pwCoverageTake().map(json => JSON.parse(json));
+ }",
+ FileCoverageJson("a.js", 3)));
+ Assert.That(first[0].Data["a.js"].StatementMap, Is.Not.Null);
+ Assert.That(first[0].Data["a.js"].S, Is.EqualTo(new Dictionary { ["0"] = 3 }));
+
+ IstanbulCoverageChunk[] second = ToChunks(await page.EvaluateAsync(
+ @"() => {
+ window.__coverage__['a.js'].s['0'] += 2;
+ return window.__pwCoverageTake().map(json => JSON.parse(json));
+ }"));
+ Assert.That(second[0].Data["a.js"].StatementMap, Is.Null);
+ Assert.That(second[0].Data["a.js"].S, Is.EqualTo(new Dictionary { ["0"] = 2 }));
+
+ // Nothing was hit since the last report.
+ Assert.That(await page.EvaluateAsync("() => window.__pwCoverageTake()"), Is.Empty);
+
+ await context.Tracing.StopAsync();
+ await context.CloseAsync();
+ }
+
+ [PlaywrightTest("coverage.spec.ts", "should accumulate counters across pulls and keep never hit files")]
+ [Test, Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldAccumulateCountersAcrossPullsAndKeepNeverHitFiles()
+ {
+ IBrowserContext context = await Browser.NewContextAsync();
+ await context.Tracing.StartAsync(new TracingStartOptions { Coverage = true });
+ IPage page = await context.NewPageAsync();
+ await page.SetContentAsync(CoverageScript("a.js", 0));
+ await page.EvaluateAsync("() => window.__coverage__['a.js'].s['0'] += 4");
+ await page.EvaluateAsync("() => window.__coverage__['a.js'].s['0'] += 3");
+
+ string traceFile = OutputPath("trace.zip");
+ await context.Tracing.StopAsync(new() { Path = traceFile });
+ await context.CloseAsync();
+
+ Dictionary data = ParseTraceCoverage(traceFile);
+ Assert.That(data["a.js"].S["0"], Is.EqualTo(7));
+ Assert.That(data["a.js"].StatementMap.Keys, Is.EqualTo(new[] { "0" }));
+ }
+
+ [PlaywrightTest("coverage.spec.ts", "should collect coverage of a page closed by in-page script")]
+ [Test, Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldCollectCoverageOfAPageClosedByInPageScript()
+ {
+ IBrowserContext context = await Browser.NewContextAsync();
+ await context.Tracing.StartAsync(new TracingStartOptions { Coverage = true });
+ IPage page = await context.NewPageAsync();
+ await page.GoToAsync(TestConstants.EmptyPage);
+
+ IPage popup = await OpenPopupAsync(page, "popup.js", 5);
+
+ // An in-page close never reaches Playwright as page.close(), the counters
+ // are preserved because the actions collect them as they go.
+ await Task.WhenAll(
+ popup.WaitForCloseAsync(),
+ popup.EvaluateAsync("() => setTimeout(() => window.close(), 0)"));
+
+ string traceFile = OutputPath("trace.zip");
+ await context.Tracing.StopAsync(new() { Path = traceFile });
+ await context.CloseAsync();
+
+ Dictionary data = ParseTraceCoverage(traceFile);
+ Assert.That(data["popup.js"].S["0"], Is.EqualTo(5));
+ }
+
+ [PlaywrightTest("coverage.spec.ts", "should not double count a stash picked up twice")]
+ [Test, Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldNotDoubleCountAStashPickedUpTwice()
+ {
+ IBrowserContext context = await Browser.NewContextAsync();
+ await context.Tracing.StartAsync(new TracingStartOptions { Coverage = true });
+ IPage page = await context.NewPageAsync();
+ await page.GoToAsync(TestConstants.EmptyPage);
+
+ // Emulate two documents picking up the same stash before either removes it.
+ string prefix = "__pwCoverage." + ((IHasOfficialTrace)context).OfficialTrace.CoverageSessionId + ".";
+ await page.EvaluateAsync(
+ @"({ prefix, coverage }) => {
+ const chunk = JSON.stringify({ id: 'stash-id', data: JSON.parse(coverage) });
+ localStorage.setItem(prefix + 'one', chunk);
+ localStorage.setItem(prefix + 'two', chunk);
+ }",
+ new { prefix, coverage = FileCoverageJson("stashed.js", 4) });
+
+ string traceFile = OutputPath("trace.zip");
+ await context.Tracing.StopAsync(new() { Path = traceFile });
+ await context.CloseAsync();
+
+ Dictionary data = ParseTraceCoverage(traceFile);
+ Assert.That(data["stashed.js"].S["0"], Is.EqualTo(4));
+ }
+
+ [PlaywrightTest("coverage.spec.ts", "should discard stashes of other sessions")]
+ [Test, Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldDiscardStashesOfOtherSessions()
+ {
+ IBrowserContext context = await Browser.NewContextAsync();
+ await context.Tracing.StartAsync(new TracingStartOptions { Coverage = true });
+ IPage page = await context.NewPageAsync();
+ await page.GoToAsync(TestConstants.EmptyPage);
+ await page.EvaluateAsync(
+ @"coverage => {
+ localStorage.setItem('__pwCoverage.other-session.1', JSON.stringify({ id: 'other', data: JSON.parse(coverage) }));
+ }",
+ FileCoverageJson("stale.js", 7));
+
+ string[] remaining = await page.EvaluateAsync("() => Object.keys(localStorage).filter(key => key.startsWith('__pwCoverage.'))");
+ Assert.That(remaining, Is.Empty);
+
+ string traceFile = OutputPath("trace.zip");
+ await context.Tracing.StopAsync(new() { Path = traceFile });
+ await context.CloseAsync();
+
+ // The stale stash was the only coverage, so nothing was collected at all.
+ Assert.That(ParseTraceCoverage(traceFile), Is.Null);
+ }
+
+ [PlaywrightTest("coverage.spec.ts", "should collect coverage of an origin left without a page")]
+ [Test, Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldCollectCoverageOfAnOriginLeftWithoutAPage()
+ {
+ IBrowserContext context = await Browser.NewContextAsync();
+ await context.Tracing.StartAsync(new TracingStartOptions { Coverage = true });
+ IPage page = await context.NewPageAsync();
+ await page.GoToAsync(TestConstants.EmptyPage);
+
+ IPage popup = await OpenPopupAsync(page, "popup.js", 0);
+ await popup.EvaluateAsync(
+ @"url => {
+ window.__go = () => {
+ window.__coverage__['popup.js'].s['0'] = 5;
+ window.location.href = url;
+ };
+ }",
+ TestConstants.CrossProcessHttpPrefix + "/empty.html");
+
+ // The popup is hit and navigates away, leaving a stash behind. Triggered from
+ // the opener, so that the pull after the action does not race the navigation.
+ await Task.WhenAll(
+ popup.WaitForURLAsync(TestConstants.CrossProcessHttpPrefix + "/empty.html"),
+ page.EvaluateAsync("() => window.__popup.__go()"));
+
+ // Leave the origin of the stash without a page to relay it.
+ await page.GoToAsync(TestConstants.CrossProcessHttpPrefix + "/empty.html");
+
+ string traceFile = OutputPath("trace.zip");
+ await context.Tracing.StopAsync(new() { Path = traceFile });
+ await context.CloseAsync();
+
+ Dictionary data = ParseTraceCoverage(traceFile);
+ Assert.That(data["popup.js"].S["0"], Is.EqualTo(5));
+ }
+
+ [PlaywrightTest("coverage.spec.ts", "should pull counters as the actions go")]
+ [Test, Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldPullCountersAsTheActionsGo()
+ {
+ IBrowserContext context = await Browser.NewContextAsync();
+ await context.Tracing.StartAsync(new TracingStartOptions { Coverage = true });
+ IPage page = await context.NewPageAsync();
+ await page.GoToAsync(TestConstants.EmptyPage);
+ await page.SetContentAsync(CoverageScript("a.js", 3));
+
+ // The counters are collected by the action itself, not only by the final flush.
+ Assert.That(await page.EvaluateAsync("() => window.__coverage__['a.js'].s['0']"), Is.EqualTo(0));
+
+ string traceFile = OutputPath("trace.zip");
+ await context.Tracing.StopAsync(new() { Path = traceFile });
+ await context.CloseAsync();
+
+ Dictionary data = ParseTraceCoverage(traceFile);
+ Assert.That(data["a.js"].S["0"], Is.EqualTo(3));
+ }
+
+ [PlaywrightTest("coverage.spec.ts", "should not collect coverage without the option")]
+ [Test, Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldNotCollectCoverageWithoutTheOption()
+ {
+ IBrowserContext context = await Browser.NewContextAsync();
+ await context.Tracing.StartAsync();
+ IPage page = await context.NewPageAsync();
+ await page.SetContentAsync(CoverageScript("a.js", 1));
+
+ string traceFile = OutputPath("trace.zip");
+ await context.Tracing.StopAsync(new() { Path = traceFile });
+ await context.CloseAsync();
+
+ Assert.That(ParseTraceCoverage(traceFile), Is.Null);
+ }
+
+ [PlaywrightTest("coverage.spec.ts", "should count only the hits after start")]
+ [Test, Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldCountOnlyTheHitsAfterStart()
+ {
+ IBrowserContext context = await Browser.NewContextAsync();
+ IPage page = await context.NewPageAsync();
+ await page.GoToAsync(TestConstants.EmptyPage);
+ await page.EvaluateAsync("coverage => window.__coverage__ = JSON.parse(coverage)", FileCoverageJson("a.js", 3));
+
+ await context.Tracing.StartAsync(new TracingStartOptions { Coverage = true });
+ await page.EvaluateAsync("() => window.__coverage__['a.js'].s['0'] += 2");
+ string traceFile = OutputPath("trace.zip");
+ await context.Tracing.StopAsync(new() { Path = traceFile });
+ await context.CloseAsync();
+
+ Dictionary data = ParseTraceCoverage(traceFile);
+ Assert.That(data["a.js"].S["0"], Is.EqualTo(2));
+ Assert.That(data["a.js"].StatementMap.Keys, Is.EqualTo(new[] { "0" }));
+ }
+
+ [PlaywrightTest("coverage.spec.ts", "should stop collecting when tracing stops")]
+ [Test, Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldStopCollectingWhenTracingStops()
+ {
+ IBrowserContext context = await Browser.NewContextAsync();
+ await context.Tracing.StartAsync(new TracingStartOptions { Coverage = true });
+ IPage page = await context.NewPageAsync();
+ await page.GoToAsync(TestConstants.EmptyPage);
+ await page.EvaluateAsync("coverage => window.__coverage__ = JSON.parse(coverage)", FileCoverageJson("a.js", 1));
+ await context.Tracing.StopAsync();
+
+ Assert.That(await page.EvaluateAsync("() => typeof window.__pwCoverageTake"), Is.EqualTo("undefined"));
+
+ // Leaving the document no longer stashes the counters.
+ await page.EvaluateAsync("() => window.__coverage__['a.js'].s['0'] = 5");
+ await page.GoToAsync(TestConstants.ServerUrl + "/title.html");
+ Assert.That(await page.EvaluateAsync("() => Object.keys(localStorage).filter(key => key.startsWith('__pwCoverage.'))"), Is.Empty);
+ Assert.That(await page.EvaluateAsync("() => typeof window.__pwCoverageTake"), Is.EqualTo("undefined"));
+ await context.CloseAsync();
+ }
+
+ [PlaywrightTest("coverage.spec.ts", "should surface a failure to stash the coverage")]
+ [Test, Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldSurfaceAFailureToStashTheCoverage()
+ {
+ IBrowserContext context = await Browser.NewContextAsync();
+ await context.Tracing.StartAsync(new TracingStartOptions { Coverage = true });
+ IPage page = await context.NewPageAsync();
+ await page.GoToAsync(TestConstants.EmptyPage);
+ await page.EvaluateAsync(
+ @"coverage => {
+ window.__coverage__ = JSON.parse(coverage);
+ // The stash does not fit, the error record still does.
+ const setItem = Storage.prototype.setItem;
+ Storage.prototype.setItem = function(key, value) {
+ if (value.length > 100)
+ throw new DOMException('The quota has been exceeded.', 'QuotaExceededError');
+ setItem.call(this, key, value);
+ };
+ window.dispatchEvent(new Event('pagehide'));
+ }",
+ FileCoverageJson("a.js", 3));
+
+ PlaywrightException error = Assert.ThrowsAsync(() => context.Tracing.StopAsync(new() { Path = OutputPath("trace.zip") }));
+ Assert.That(error.Message, Does.Contain("Failed to stash the coverage: QuotaExceededError: The quota has been exceeded."));
+ await context.CloseAsync();
+ }
+
+ private static string FileCoverageJson(string file, int s0)
+ => JsonSerializer.Serialize(new Dictionary
+ {
+ [file] = new
+ {
+ path = file,
+ statementMap = new Dictionary
+ {
+ ["0"] = new { start = new { line = 1, column = 0 }, end = new { line = 1, column = 20 } },
+ },
+ fnMap = new { },
+ branchMap = new { },
+ s = new Dictionary { ["0"] = s0 },
+ f = new { },
+ b = new { },
+ },
+ });
+
+ private static string CoverageScript(string file, int s0)
+ => "";
+
+ private static IstanbulCoverageChunk[] ToChunks(JsonElement value)
+ => JsonSerializer.Deserialize(value.GetRawText());
+
+ private static Dictionary ParseTraceCoverage(string file)
+ {
+ using ZipArchive zip = ZipFile.OpenRead(file);
+ ZipArchiveEntry[] entries = zip.Entries.Where(entry => Regex.IsMatch(entry.FullName, @"(^|-)trace\.coverage$")).ToArray();
+ if (entries.Length == 0)
+ {
+ return null;
+ }
+
+ Dictionary coverage = new(StringComparer.Ordinal);
+ foreach (ZipArchiveEntry entry in entries)
+ {
+ using StreamReader reader = new(entry.Open());
+ IstanbulCoverage.Merge(coverage, IstanbulCoverage.Parse(reader.ReadToEnd()));
+ }
+
+ return coverage;
+ }
+
+ private static async Task OpenPopupAsync(IPage page, string file, int s0)
+ {
+ Task popupTask = page.WaitForPopupAsync();
+ await Task.WhenAll(
+ popupTask,
+ page.EvaluateAsync("url => window.__popup = window.open(url)", TestConstants.EmptyPage));
+ IPage popup = await popupTask;
+ await popup.EvaluateAsync("coverage => window.__coverage__ = JSON.parse(coverage)", FileCoverageJson(file, s0));
+ return popup;
+ }
+
+ private string OutputPath(string name) => Path.Combine(_outputDir, name);
+ }
+}
diff --git a/src/PlaywrightNative/Chromium/CRTracing.cs b/src/PlaywrightNative/Chromium/CRTracing.cs
index 911a23d0..9d5f0451 100644
--- a/src/PlaywrightNative/Chromium/CRTracing.cs
+++ b/src/PlaywrightNative/Chromium/CRTracing.cs
@@ -50,10 +50,10 @@ internal CRTracing(CRSession session, IBrowserContext context)
}
///
- public Task StartAsync(PlaywrightNative.TracingStartOptions options)
+ public async Task StartAsync(PlaywrightNative.TracingStartOptions options)
{
OfficialTraceSession session = OfficialSession();
- session.Start(options, chunk: false);
+ await session.StartAsync(options, chunk: false).ConfigureAwait(false);
// Keep an in-memory chrome-events buffer for Direct StopAsync(.json) paths
// (groups / non-empty traceEvents). Official zip remains for .zip stops.
@@ -65,8 +65,6 @@ public Task StartAsync(PlaywrightNative.TracingStartOptions options)
_events.Add(ChromeTraceEvents.GroupBegin("__tracing__"));
_events.Add(ChromeTraceEvents.GroupEnd("__tracing__"));
}
-
- return Task.CompletedTask;
}
Task ITracing.StartAsync(Microsoft.Playwright.TracingStartOptions options)
@@ -98,12 +96,12 @@ public async Task StartAsync(string categories = null)
}
///
- public Task StartChunkAsync(string name = default, string title = default)
+ public async Task StartChunkAsync(string name = default, string title = default)
{
OfficialTraceSession official = OfficialSessionOrNull();
if (official != null && official.IsRecording)
{
- official.Start(new TracingStartOptions { Name = name, Title = title }, chunk: true);
+ await official.StartAsync(new TracingStartOptions { Name = name, Title = title }, chunk: true).ConfigureAwait(false);
lock (_gate)
{
_events.Clear();
@@ -113,7 +111,7 @@ public Task StartChunkAsync(string name = default, string title = default)
_events.Add(ChromeTraceEvents.GroupEnd("__tracing__"));
}
- return Task.CompletedTask;
+ return;
}
_ = name;
@@ -127,8 +125,6 @@ public Task StartChunkAsync(string name = default, string title = default)
_events.Clear();
}
-
- return Task.CompletedTask;
}
///
diff --git a/src/PlaywrightNative/Contracts/Models/TracingStartOptions.cs b/src/PlaywrightNative/Contracts/Models/TracingStartOptions.cs
index 1b8b44d0..29f39215 100644
--- a/src/PlaywrightNative/Contracts/Models/TracingStartOptions.cs
+++ b/src/PlaywrightNative/Contracts/Models/TracingStartOptions.cs
@@ -42,6 +42,7 @@ public TracingStartOptions(TracingStartOptions clone)
ScreenSnapshots = clone.ScreenSnapshots;
AriaSnapshots = clone.AriaSnapshots;
+ Coverage = clone.Coverage;
}
///
@@ -55,5 +56,11 @@ public TracingStartOptions(TracingStartOptions clone)
/// with before/action/after phases.
///
public bool? AriaSnapshots { get; set; }
+
+ ///
+ /// Official hidden _coverage option. Collects istanbul
+ /// window.__coverage__ counters into the trace.
+ ///
+ internal bool? Coverage { get; set; }
}
}
diff --git a/src/PlaywrightNative/Helpers/ActionTrace.cs b/src/PlaywrightNative/Helpers/ActionTrace.cs
index c1ae4df8..48639b7a 100644
--- a/src/PlaywrightNative/Helpers/ActionTrace.cs
+++ b/src/PlaywrightNative/Helpers/ActionTrace.cs
@@ -35,7 +35,7 @@ internal static IDisposable SuppressRecording()
return new SuppressScope();
}
- internal static Task RunAsync(IBrowserContext context, string title, string className, string method, Func body, object parameters = null, object result = null)
+ internal static Task RunAsync(IBrowserContext context, string title, string className, string method, Func body, object parameters = null, object result = null, IPage page = null)
{
OfficialTraceSession session = OfficialTraceSession.Active(context);
if (session == null || Suppress.Value > 0 || Depth.Value > 0)
@@ -52,10 +52,10 @@ internal static Task RunAsync(IBrowserContext context, string title, string clas
return body();
}
- return RunInsideAsync(() => session.ContinueActionAsync(callId, body, result));
+ return RunInsideAsync(() => session.ContinueActionAsync(callId, body, result, page));
}
- internal static Task RunAsync(IBrowserContext context, string title, string className, string method, Func> body, object parameters = null, object result = null)
+ internal static Task RunAsync(IBrowserContext context, string title, string className, string method, Func> body, object parameters = null, object result = null, IPage page = null)
{
OfficialTraceSession session = OfficialTraceSession.Active(context);
if (session == null || Suppress.Value > 0 || Depth.Value > 0)
@@ -69,10 +69,10 @@ internal static Task RunAsync(IBrowserContext context, string title, strin
return body();
}
- return RunInsideAsync(() => session.ContinueActionAsync(callId, body, result));
+ return RunInsideAsync(() => session.ContinueActionAsync(callId, body, result, page));
}
- internal static Task EvaluateUserAsync(IBrowserContext context, Func> body, int consoleMessageCount = 0)
+ internal static Task EvaluateUserAsync(IBrowserContext context, Func> body, int consoleMessageCount = 0, IPage page = null)
{
if (body == null)
{
@@ -92,10 +92,10 @@ internal static Task EvaluateUserAsync(IBrowserContext context, Func session.ContinueActionAsync(callId, body, null));
+ return RunInsideAsync(() => session.ContinueActionAsync(callId, body, null, page));
}
- internal static Task EvaluateHandleUserAsync(IBrowserContext context, Func> body, int consoleMessageCount = 0)
+ internal static Task EvaluateHandleUserAsync(IBrowserContext context, Func> body, int consoleMessageCount = 0, IPage page = null)
{
if (body == null)
{
@@ -115,7 +115,7 @@ internal static Task EvaluateHandleUserAsync(IBrowserContext context, Func
}
session.NoteEvaluateConsoleBaseline(callId, consoleMessageCount);
- return RunInsideAsync(() => session.ContinueActionAsync(callId, body, null));
+ return RunInsideAsync(() => session.ContinueActionAsync(callId, body, null, page));
}
internal static string NavigateTitle(string url)
diff --git a/src/PlaywrightNative/Helpers/CoverageRecorder.cs b/src/PlaywrightNative/Helpers/CoverageRecorder.cs
new file mode 100644
index 00000000..c5a6e0a6
--- /dev/null
+++ b/src/PlaywrightNative/Helpers/CoverageRecorder.cs
@@ -0,0 +1,286 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ * Modifications copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.Playwright;
+
+namespace PlaywrightNative.Helpers
+{
+ ///
+ /// Collects istanbul window.__coverage__ counters into the trace.
+ /// Port of packages/playwright-core/src/server/coverageRecorder.ts.
+ ///
+ internal sealed class CoverageRecorder
+ {
+ private const string TakeName = "__pwCoverageTake";
+
+ private const string TakeExpression = "window[\"" + TakeName + "\"] ? window[\"" + TakeName + "\"]() : []";
+
+ private const string DisposeExpression = "window[\"" + TakeName + "\"]?.dispose()";
+
+ private static readonly Lazy Source = new(LoadSource);
+
+ private readonly object _gate = new();
+ private readonly IBrowserContext _context;
+ private readonly string _sessionId;
+ private readonly Dictionary _coverage = new(StringComparer.Ordinal);
+ private readonly HashSet _stashedChunkIds = new(StringComparer.Ordinal);
+ private PlaywrightException _stashError;
+ private IAsyncDisposable _initScript;
+
+ internal CoverageRecorder(IBrowserContext context, string sessionId)
+ {
+ _context = context;
+ _sessionId = JsonSerializer.Serialize(sessionId);
+ }
+
+ internal async Task InstallAsync()
+ {
+ if (_initScript != null)
+ {
+ return;
+ }
+
+ // The WebKit setContent replays the init scripts on the written document,
+ // a second instance would reset the counters the page already hit.
+ string source = ModuleExpression("if (!window[" + JsonSerializer.Serialize(TakeName) + "]) new (module.exports.CoverageScript())(window, " + JsonSerializer.Serialize(TakeName) + ", " + _sessionId + ");");
+ using (ActionTrace.SuppressRecording())
+ {
+ _initScript = await _context.AddInitScriptAsync(source).ConfigureAwait(false);
+ await EvaluateInAllFramesAsync(source).ConfigureAwait(false);
+ }
+ }
+
+ internal async Task UninstallAsync()
+ {
+ using (ActionTrace.SuppressRecording())
+ {
+ try
+ {
+ await Task.WhenAll(
+ _initScript?.DisposeAsync().AsTask() ?? Task.CompletedTask,
+ EvaluateInAllFramesAsync(DisposeExpression)).ConfigureAwait(false);
+ }
+ catch (PlaywrightException)
+ {
+ }
+ }
+ }
+
+ ///
+ /// Returns the coverage collected since the last take, serialized, or
+ /// when there is none.
+ ///
+ /// Drops the collected coverage without pulling.
+ /// The coverage JSON.
+ internal async Task TakeAsync(bool discard)
+ {
+ if (discard)
+ {
+ lock (_gate)
+ {
+ _coverage.Clear();
+ }
+
+ return null;
+ }
+
+ await FlushAsync().ConfigureAwait(false);
+ lock (_gate)
+ {
+ if (_stashError != null)
+ {
+ throw _stashError;
+ }
+
+ _stashedChunkIds.Clear();
+ if (_coverage.Count == 0)
+ {
+ return null;
+ }
+
+ string json = IstanbulCoverage.Serialize(_coverage);
+ _coverage.Clear();
+ return json;
+ }
+ }
+
+ internal async Task CollectFromPageAsync(IPage page)
+ {
+ if (page == null || page.IsClosed)
+ {
+ return;
+ }
+
+ using (ActionTrace.SuppressRecording())
+ {
+ await Task.WhenAll(page.Frames.Select(async frame =>
+ {
+ foreach (string json in await TakeFromFrameAsync(frame, TakeExpression).ConfigureAwait(false))
+ {
+ Append(json);
+ }
+ })).ConfigureAwait(false);
+ }
+ }
+
+ private static string ModuleExpression(string call)
+ => "(() => {\nconst module = {};\n" + Source.Value + "\n" + call + "\n})()";
+
+ private static string LoadSource()
+ {
+ Assembly assembly = typeof(CoverageRecorder).Assembly;
+ using Stream stream = assembly.GetManifestResourceStream("PlaywrightNative.Helpers.coverageScriptSource.js")
+ ?? throw new PlaywrightException("Bundled Playwright coverage script source is missing.");
+ using StreamReader reader = new StreamReader(stream);
+ return reader.ReadToEnd();
+ }
+
+ private async Task FlushAsync()
+ {
+ await Task.WhenAll(_context.Pages.Select(CollectFromPageAsync)).ConfigureAwait(false);
+ await HarvestOriginsWithoutPageAsync().ConfigureAwait(false);
+ }
+
+ private async Task HarvestOriginsWithoutPageAsync()
+ {
+ if (_context is not IHasStorageStateInternals internals || _context.IsClosed)
+ {
+ return;
+ }
+
+ HashSet liveOrigins = new(StringComparer.Ordinal);
+ foreach (IPage page in _context.Pages)
+ {
+ foreach (IFrame frame in page.Frames)
+ {
+ if (StorageStateHelper.TryGetHttpOrigin(frame.Url, out string origin))
+ {
+ liveOrigins.Add(origin);
+ }
+ }
+ }
+
+ // The origins that still have a page were drained by the sweep above.
+ HashSet origins = new(internals.VisitedOrigins.Where(origin => !liveOrigins.Contains(origin)), StringComparer.Ordinal);
+ if (origins.Count == 0)
+ {
+ return;
+ }
+
+ string source = ModuleExpression("return (module.exports.takeCoverageStashes())(window, " + _sessionId + ");");
+ using (ActionTrace.SuppressRecording())
+ {
+ await StorageStateHelper.VisitOriginsAsync(_context, origins, async (frame, _) =>
+ {
+ foreach (string json in await TakeFromFrameAsync(frame, source).ConfigureAwait(false))
+ {
+ Append(json);
+ }
+ }).ConfigureAwait(false);
+ }
+ }
+
+ private async Task TakeFromFrameAsync(IFrame frame, string expression)
+ {
+ try
+ {
+ return await frame.EvaluateAsync(expression).ConfigureAwait(false) ?? Array.Empty();
+ }
+ catch (PlaywrightException ex)
+ {
+ return TakeFailed(ex);
+ }
+ catch (TimeoutException ex)
+ {
+ return TakeFailed(ex);
+ }
+ catch (InvalidOperationException ex)
+ {
+ return TakeFailed(ex);
+ }
+ }
+
+ private Task EvaluateInAllFramesAsync(string expression)
+ => Task.WhenAll(_context.Pages.SelectMany(page => page.Frames).Select(async frame =>
+ {
+ try
+ {
+ await frame.EvaluateAsync(expression).ConfigureAwait(false);
+ }
+ catch (PlaywrightException)
+ {
+ }
+ catch (TimeoutException)
+ {
+ }
+ catch (InvalidOperationException)
+ {
+ }
+ }));
+
+ // Evaluation fails for pages on their way out, only the stash error matters.
+ private string[] TakeFailed(Exception error)
+ {
+ string message = error.Message ?? string.Empty;
+ int start = message.IndexOf(IstanbulCoverage.StashError, StringComparison.Ordinal);
+ if (start >= 0)
+ {
+ message = message.Substring(start).Split('\n')[0];
+ lock (_gate)
+ {
+ _stashError ??= new PlaywrightException(message);
+ }
+ }
+
+ return Array.Empty();
+ }
+
+ private void Append(string json)
+ {
+ IstanbulCoverageChunk chunk;
+ try
+ {
+ chunk = IstanbulCoverage.ParseChunk(json);
+ }
+ catch (JsonException)
+ {
+ return;
+ }
+
+ if (chunk?.Data == null)
+ {
+ return;
+ }
+
+ lock (_gate)
+ {
+ if (chunk.Id != null && !_stashedChunkIds.Add(chunk.Id))
+ {
+ return;
+ }
+
+ IstanbulCoverage.Merge(_coverage, chunk.Data);
+ }
+ }
+ }
+}
diff --git a/src/PlaywrightNative/Helpers/EmptyTracing.cs b/src/PlaywrightNative/Helpers/EmptyTracing.cs
index 1aa94e5c..48d61f05 100644
--- a/src/PlaywrightNative/Helpers/EmptyTracing.cs
+++ b/src/PlaywrightNative/Helpers/EmptyTracing.cs
@@ -44,12 +44,12 @@ internal EmptyTracing(IBrowserContext context = null)
}
///
- public Task StartAsync(PlaywrightNative.TracingStartOptions options)
+ public async Task StartAsync(PlaywrightNative.TracingStartOptions options)
{
if (_context is IHasOfficialTrace host)
{
host.OfficialTrace ??= new OfficialTraceSession(_context);
- host.OfficialTrace.Start(options, chunk: false);
+ await host.OfficialTrace.StartAsync(options, chunk: false).ConfigureAwait(false);
// Also keep the chrome-events recorder so Direct StopAsync(.json)
// can write traceEvents (groups) instead of an official zip.
@@ -60,7 +60,7 @@ public Task StartAsync(PlaywrightNative.TracingStartOptions options)
_openGroups.Clear();
}
- return Task.CompletedTask;
+ return;
}
// Standalone APIRequest tracing has no browser context. Still start
@@ -76,7 +76,7 @@ public Task StartAsync(PlaywrightNative.TracingStartOptions options)
_openGroups.Clear();
}
- return Task.CompletedTask;
+ return;
}
OwnOfficial().Start(options, chunk: false);
@@ -86,8 +86,6 @@ public Task StartAsync(PlaywrightNative.TracingStartOptions options)
_events.Clear();
_openGroups.Clear();
}
-
- return Task.CompletedTask;
}
Task ITracing.StartAsync(Microsoft.Playwright.TracingStartOptions options)
@@ -108,12 +106,12 @@ public Task StartAsync(string categories = null)
}
///
- public Task StartChunkAsync(string name = default, string title = default)
+ public async Task StartChunkAsync(string name = default, string title = default)
{
OfficialTraceSession official = OfficialSessionOrNull();
if (official != null && official.IsRecording)
{
- official.Start(new TracingStartOptions { Name = name, Title = title }, chunk: true);
+ await official.StartAsync(new TracingStartOptions { Name = name, Title = title }, chunk: true).ConfigureAwait(false);
lock (_gate)
{
_events.Clear();
@@ -121,12 +119,11 @@ public Task StartChunkAsync(string name = default, string title = default)
_recording = true;
}
- return Task.CompletedTask;
+ return;
}
_ = name;
_ = title;
- return Task.CompletedTask;
}
///
diff --git a/src/PlaywrightNative/Helpers/IstanbulBranchMapping.cs b/src/PlaywrightNative/Helpers/IstanbulBranchMapping.cs
new file mode 100644
index 00000000..6bb18f2a
--- /dev/null
+++ b/src/PlaywrightNative/Helpers/IstanbulBranchMapping.cs
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ * Modifications copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace PlaywrightNative.Helpers
+{
+ /// A branch mapping. Only the number of locations matters when merging.
+ internal sealed class IstanbulBranchMapping
+ {
+ /// Gets or sets the branch locations.
+ [JsonPropertyName("locations")]
+ public List Locations { get; set; } = new();
+
+ /// Gets or sets the other mapping fields, kept as reported.
+ [JsonExtensionData]
+ public Dictionary Extra { get; set; }
+ }
+}
diff --git a/src/PlaywrightNative/Helpers/IstanbulCoverage.cs b/src/PlaywrightNative/Helpers/IstanbulCoverage.cs
new file mode 100644
index 00000000..17a43694
--- /dev/null
+++ b/src/PlaywrightNative/Helpers/IstanbulCoverage.cs
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ * Modifications copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace PlaywrightNative.Helpers
+{
+ ///
+ /// The istanbul coverage format, mirrors the shapes from istanbul-lib-coverage.
+ /// Port of packages/isomorphic/istanbulCoverage.ts.
+ ///
+ internal static class IstanbulCoverage
+ {
+ /// Prefix of the localStorage keys that hold coverage stashes.
+ internal const string StashPrefix = "__pwCoverage.";
+
+ /// Message prefix of the error raised when a stash could not be written.
+ internal const string StashError = "Failed to stash the coverage";
+
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ };
+
+ /// Parses a coverage chunk reported by the page.
+ /// The chunk JSON.
+ /// The chunk.
+ internal static IstanbulCoverageChunk ParseChunk(string json)
+ => JsonSerializer.Deserialize(json, JsonOptions);
+
+ /// Parses serialized coverage, e.g. a trace.coverage entry.
+ /// The coverage JSON.
+ /// The coverage keyed by file.
+ internal static Dictionary Parse(string json)
+ => JsonSerializer.Deserialize>(json, JsonOptions);
+
+ /// Serializes sorted by file.
+ /// The coverage keyed by file.
+ /// The coverage JSON.
+ internal static string Serialize(Dictionary coverage)
+ => JsonSerializer.Serialize(Sorted(coverage), JsonOptions);
+
+ /// Returns sorted by file.
+ /// The coverage keyed by file.
+ /// The sorted coverage.
+ internal static SortedDictionary Sorted(Dictionary coverage)
+ => new(coverage, StringComparer.Ordinal);
+
+ /// Adds the counters of into .
+ /// The accumulated coverage.
+ /// A coverage delta, the maps are only present in the first report of a file.
+ internal static void Merge(Dictionary into, IReadOnlyDictionary data)
+ {
+ foreach (KeyValuePair item in data)
+ {
+ IstanbulFileCoverage fileCov = item.Value;
+ if (!into.TryGetValue(item.Key, out IstanbulFileCoverage existing))
+ {
+ existing = new IstanbulFileCoverage { Path = fileCov.Path, StatementMap = new(), FnMap = new(), BranchMap = new() };
+ into[item.Key] = existing;
+ }
+
+ // The maps can arrive late if the report that carried them was lost.
+ if (fileCov.StatementMap != null && existing.StatementMap.Count == 0)
+ {
+ existing.StatementMap = fileCov.StatementMap;
+ existing.FnMap = fileCov.FnMap ?? new();
+ existing.BranchMap = fileCov.BranchMap ?? new();
+
+ // Reports only carry the counters that were hit, zero fill the rest.
+ foreach (string key in existing.StatementMap.Keys)
+ {
+ existing.S.TryAdd(key, 0);
+ }
+
+ foreach (string key in existing.FnMap.Keys)
+ {
+ existing.F.TryAdd(key, 0);
+ }
+
+ foreach (KeyValuePair branch in existing.BranchMap)
+ {
+ existing.B.TryAdd(branch.Key, branch.Value.Locations.Select(_ => 0L).ToList());
+ }
+ }
+
+ foreach (KeyValuePair counter in fileCov.S ?? new())
+ {
+ existing.S[counter.Key] = existing.S.GetValueOrDefault(counter.Key) + counter.Value;
+ }
+
+ foreach (KeyValuePair counter in fileCov.F ?? new())
+ {
+ existing.F[counter.Key] = existing.F.GetValueOrDefault(counter.Key) + counter.Value;
+ }
+
+ foreach (KeyValuePair> counter in fileCov.B ?? new())
+ {
+ if (!existing.B.TryGetValue(counter.Key, out List branches))
+ {
+ branches = new List();
+ existing.B[counter.Key] = branches;
+ }
+
+ for (int i = 0; i < counter.Value.Count; i++)
+ {
+ if (i < branches.Count)
+ {
+ branches[i] += counter.Value[i];
+ }
+ else
+ {
+ branches.Add(counter.Value[i]);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/PlaywrightNative/Helpers/IstanbulCoverageChunk.cs b/src/PlaywrightNative/Helpers/IstanbulCoverageChunk.cs
new file mode 100644
index 00000000..fc889efa
--- /dev/null
+++ b/src/PlaywrightNative/Helpers/IstanbulCoverageChunk.cs
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ * Modifications copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace PlaywrightNative.Helpers
+{
+ /// A coverage report of one document, or a stash it left behind.
+ internal sealed class IstanbulCoverageChunk
+ {
+ /// Gets or sets the coverage delta.
+ [JsonPropertyName("data")]
+ public Dictionary Data { get; set; }
+
+ /// Gets or sets the stash id, set only for stashes.
+ [JsonPropertyName("id")]
+ public string Id { get; set; }
+ }
+}
diff --git a/src/PlaywrightNative/Helpers/IstanbulFileCoverage.cs b/src/PlaywrightNative/Helpers/IstanbulFileCoverage.cs
new file mode 100644
index 00000000..bea60a77
--- /dev/null
+++ b/src/PlaywrightNative/Helpers/IstanbulFileCoverage.cs
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ * Modifications copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace PlaywrightNative.Helpers
+{
+ ///
+ /// Coverage of one file. In a delta the maps are
+ /// unless it is the first report of the file.
+ ///
+ internal sealed class IstanbulFileCoverage
+ {
+ /// Gets or sets the file path.
+ [JsonPropertyName("path")]
+ public string Path { get; set; }
+
+ /// Gets or sets the statement locations.
+ [JsonPropertyName("statementMap")]
+ public Dictionary StatementMap { get; set; }
+
+ /// Gets or sets the function mappings.
+ [JsonPropertyName("fnMap")]
+ public Dictionary FnMap { get; set; }
+
+ /// Gets or sets the branch mappings.
+ [JsonPropertyName("branchMap")]
+ public Dictionary BranchMap { get; set; }
+
+ /// Gets or sets the statement counters.
+ [JsonPropertyName("s")]
+ public Dictionary S { get; set; } = new();
+
+ /// Gets or sets the function counters.
+ [JsonPropertyName("f")]
+ public Dictionary F { get; set; } = new();
+
+ /// Gets or sets the branch counters.
+ [JsonPropertyName("b")]
+ public Dictionary> B { get; set; } = new();
+ }
+}
diff --git a/src/PlaywrightNative/Helpers/OfficialTraceSession.cs b/src/PlaywrightNative/Helpers/OfficialTraceSession.cs
index 99a55820..104100aa 100644
--- a/src/PlaywrightNative/Helpers/OfficialTraceSession.cs
+++ b/src/PlaywrightNative/Helpers/OfficialTraceSession.cs
@@ -50,6 +50,8 @@ internal sealed class OfficialTraceSession
private readonly List _pendingCaptures = new();
private readonly IBrowserContext _context;
private readonly bool _apiOnly;
+ private readonly string _coverageSessionId = "browser-context@" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ private CoverageRecorder _coverage;
private TracingStartOptions _options;
private bool _recording;
private bool _networkAttached;
@@ -120,6 +122,24 @@ internal bool AriaSnapshotsEnabled
}
}
+ ///
+ /// Gets the id that scopes the coverage stashes, official context.guid.
+ ///
+ internal string CoverageSessionId => _coverageSessionId;
+
+ ///
+ /// Official instrumentation.onPageWillClose: pulls the coverage of
+ /// before it goes away.
+ ///
+ /// The owning context.
+ /// The page that is closing.
+ /// A task that completes once the coverage is pulled.
+ internal static Task OnPageWillCloseAsync(IBrowserContext context, IPage page)
+ {
+ CoverageRecorder recorder = (context as IHasOfficialTrace)?.OfficialTrace?.CoverageRecorderOrNull();
+ return recorder == null ? Task.CompletedTask : recorder.CollectFromPageAsync(page);
+ }
+
///
/// Returns the official session on when it
/// is recording.
@@ -161,6 +181,7 @@ internal void Start(TracingStartOptions options, bool chunk)
next.Snapshots ??= _options.Snapshots;
next.ScreenSnapshots ??= _options.ScreenSnapshots;
next.AriaSnapshots ??= _options.AriaSnapshots;
+ next.Coverage ??= _options.Coverage;
next.Sources ??= _options.Sources;
if (string.IsNullOrEmpty(next.Name))
{
@@ -214,6 +235,10 @@ internal void Start(TracingStartOptions options, bool chunk)
_name = string.IsNullOrEmpty(_options.Name) ? "trace" : _options.Name;
WriteContextOptions();
+ if (_options.Coverage == true && !_apiOnly && _context != null)
+ {
+ _coverage ??= new CoverageRecorder(_context, _coverageSessionId);
+ }
}
if (!chunk && !_apiOnly)
@@ -222,6 +247,28 @@ internal void Start(TracingStartOptions options, bool chunk)
}
}
+ ///
+ /// Starts tracing or a new chunk, then installs the coverage script when
+ /// the _coverage option is set.
+ ///
+ /// The tracing options.
+ /// Whether this starts a new chunk.
+ /// A task that completes once tracing started.
+ internal async Task StartAsync(TracingStartOptions options, bool chunk)
+ {
+ Start(options, chunk);
+ CoverageRecorder recorder;
+ lock (_gate)
+ {
+ recorder = _options?.Coverage == true ? _coverage : null;
+ }
+
+ if (recorder != null)
+ {
+ await recorder.InstallAsync().ConfigureAwait(false);
+ }
+ }
+
internal async Task RecordActionAsync(string title, string className, string method, Func body, object parameters = null, object result = null)
{
await RecordActionAsync