diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 56d65e4f18..c8b4aa7654 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,6 +9,8 @@ on: branches: - main - release-* +env: + DEBUG: pw:dotnet jobs: test-net5: diff --git a/src/Common/Version.props b/src/Common/Version.props index c616685055..7079d99cfb 100644 --- a/src/Common/Version.props +++ b/src/Common/Version.props @@ -2,7 +2,7 @@ 1.17.1 $(AssemblyVersion)-next-1 - 1.18.0-alpha-1638917105000 + 1.18.0-beta-1642018269000 $(AssemblyVersion) $(AssemblyVersion) true diff --git a/src/Playwright.Tests/BrowserTypeConnectTests.cs b/src/Playwright.Tests/BrowserTypeConnectTests.cs index e615826b5b..cdc426fe48 100644 --- a/src/Playwright.Tests/BrowserTypeConnectTests.cs +++ b/src/Playwright.Tests/BrowserTypeConnectTests.cs @@ -452,7 +452,7 @@ public async Task ShouldRecordContextTraces() var context = await browser.NewContextAsync(); var page = await context.NewPageAsync(); - await context.Tracing.StartAsync(); + await context.Tracing.StartAsync(new() { Sources = true }); await page.GotoAsync(Server.EmptyPage); await page.SetContentAsync(""); await page.ClickAsync("button"); @@ -464,6 +464,7 @@ public async Task ShouldRecordContextTraces() ZipFile.ExtractToDirectory(tracePath, tempDirectory.Path); Assert.That(tempDirectory.Path + "/trace.trace", Does.Exist); Assert.That(tempDirectory.Path + "/trace.network", Does.Exist); + Assert.AreEqual(1, Directory.GetFiles(Path.Join(tempDirectory.Path, "resources"), "*.txt").Length); } private class RemoteServer diff --git a/src/Playwright.Tests/Locator/LocatorQueryTests.cs b/src/Playwright.Tests/Locator/LocatorQueryTests.cs index 8f8a0fc627..f79c6171fb 100644 --- a/src/Playwright.Tests/Locator/LocatorQueryTests.cs +++ b/src/Playwright.Tests/Locator/LocatorQueryTests.cs @@ -87,42 +87,42 @@ public async Task ShouldThrowDueToStrictness2() public async Task ShouldFilterByText() { await Page.SetContentAsync("
Foobar
Bar
"); - StringAssert.Contains(await Page.Locator("div").WithText("Foo").TextContentAsync(), "Foobar"); + StringAssert.Contains(await Page.Locator("div", new() { HasTextString = "Foo" }).TextContentAsync(), "Foobar"); } [PlaywrightTest("locator-query.spec.ts", "should filter by text 2")] public async Task ShouldFilterByText2() { await Page.SetContentAsync("
foo hello world bar
"); - StringAssert.Contains(await Page.Locator("div").WithText("hello world").TextContentAsync(), "foo hello world bar"); + StringAssert.Contains(await Page.Locator("div", new() { HasTextString = "hello world" }).TextContentAsync(), "foo hello world bar"); } [PlaywrightTest("locator-query.spec.ts", "should filter by regex")] public async Task ShouldFilterByRegex() { await Page.SetContentAsync("
Foobar
Bar
"); - StringAssert.Contains(await Page.Locator("div").WithText(new Regex("Foo.*")).InnerTextAsync(), "Foobar"); + StringAssert.Contains(await Page.Locator("div", new() { HasTextRegex = new Regex("Foo.*") }).InnerTextAsync(), "Foobar"); } [PlaywrightTest("locator-query.spec.ts", "should filter by text with quotes")] public async Task ShouldFilterByTextWithQuotes() { await Page.SetContentAsync("
Hello \"world\"
Hello world
"); - StringAssert.Contains(await Page.Locator("div").WithText("Hello \"world\"").InnerTextAsync(), "Hello \"world\""); + StringAssert.Contains(await Page.Locator("div", new() { HasTextString = "Hello \"world\"" }).InnerTextAsync(), "Hello \"world\""); } [PlaywrightTest("locator-query.spec.ts", "should filter by regex with quotes")] public async Task ShouldFilterByRegexWithQuotes() { await Page.SetContentAsync("
Hello \"world\"
Hello world
"); - StringAssert.Contains(await Page.Locator("div").WithText(new Regex("Hello \"world\"")).InnerTextAsync(), "Hello \"world\""); + StringAssert.Contains(await Page.Locator("div", new() { HasTextRegex = new Regex("Hello \"world\"") }).InnerTextAsync(), "Hello \"world\""); } [PlaywrightTest("locator-query.spec.ts", "should filter by regex and regexp flags")] public async Task ShouldFilterByRegexandRegexpFlags() { await Page.SetContentAsync("
Hello \"world\"
Hello world
"); - StringAssert.Contains(await Page.Locator("div").WithText(new Regex("hElLo \"wOrld\"", RegexOptions.IgnoreCase)).InnerTextAsync(), "Hello \"world\""); + StringAssert.Contains(await Page.Locator("div", new() { HasTextRegex = new Regex("hElLo \"wOrld\"", RegexOptions.IgnoreCase) }).InnerTextAsync(), "Hello \"world\""); } } } diff --git a/src/Playwright.Tests/PageWaitForRequestTests.cs b/src/Playwright.Tests/PageWaitForRequestTests.cs index 92b53bfb93..1ed0d62077 100644 --- a/src/Playwright.Tests/PageWaitForRequestTests.cs +++ b/src/Playwright.Tests/PageWaitForRequestTests.cs @@ -72,11 +72,12 @@ public Task ShouldRespectTimeout() } [PlaywrightTest("page-wait-for-request.spec.ts", "should respect default timeout")] - public Task ShouldRespectDefaultTimeout() + public async Task ShouldRespectDefaultTimeout() { Page.SetDefaultTimeout(1); - return PlaywrightAssert.ThrowsAsync( + var exception = await PlaywrightAssert.ThrowsAsync( () => Page.WaitForRequestAsync(_ => false)); + StringAssert.Contains(exception.Message, "Timeout 1ms exceeded while waiting for event \"Request\""); } [PlaywrightTest("page-wait-for-request.spec.ts", "should work with no timeout")] diff --git a/src/Playwright.Tests/TapTests.cs b/src/Playwright.Tests/TapTests.cs index 7da1fcb36b..ba49fa9cc1 100644 --- a/src/Playwright.Tests/TapTests.cs +++ b/src/Playwright.Tests/TapTests.cs @@ -72,7 +72,7 @@ await Page.SetContentAsync( await Page.TapAsync("#b", new() { Trial = true }); string[] result = await handle.JsonValueAsync(); - Assert.AreEqual(result, new string[] { }); + Assert.AreEqual(result, new string[] { "pointerover", "pointerenter", "pointerout", "pointerleave" }); } [PlaywrightTest("tap.spec.ts", "should not send mouse events touchstart is canceled")] diff --git a/src/Playwright.Tests/TracingTests.cs b/src/Playwright.Tests/TracingTests.cs index 1a4078f7a3..955995dbdd 100644 --- a/src/Playwright.Tests/TracingTests.cs +++ b/src/Playwright.Tests/TracingTests.cs @@ -23,9 +23,11 @@ */ using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; @@ -58,7 +60,7 @@ await Context.Tracing.StartAsync(new() var tracePath = Path.Combine(tmp.Path, "trace.zip"); await Context.Tracing.StopAsync(new() { Path = tracePath }); - var events = ParseTrace(tracePath); + var (events, resources) = ParseTrace(tracePath); CollectionAssert.IsNotEmpty(events); Assert.AreEqual("context-options", events[0].Type); @@ -73,25 +75,6 @@ await Context.Tracing.StartAsync(new() Assert.GreaterOrEqual(events.Where(x => x.Type == "screencast-frame").Count(), 1); } - [PlaywrightTest("tracing.spec.ts", "should exclude internal pages")] - [Ignore("Fails due to https://github.com/microsoft/playwright/issues/6743")] - public async Task ShouldExcludeInternalPages() - { - var page = await Context.NewPageAsync(); - await page.GotoAsync(Server.EmptyPage); - - await Context.Tracing.StartAsync(); - await Context.StorageStateAsync(); - await page.CloseAsync(); - - using var tmp = new TempDirectory(); - var tracePath = Path.Combine(tmp.Path, "trace.zip"); - await Context.Tracing.StopAsync(new() { Path = tracePath }); - var trace = ParseTrace(tracePath); - - Assert.AreEqual(1, trace.Where(x => x.Metadata != null).Select(x => x.Metadata.PageId).Distinct().Count()); - } - [PlaywrightTest("tracing.spec.ts", "should collect two traces")] public async Task ShouldCollectTwoTraces() { @@ -112,7 +95,7 @@ public async Task ShouldCollectTwoTraces() await Context.Tracing.StopAsync(new() { Path = trace2Path }); { - var events = ParseTrace(trace1Path); + var (events, resources) = ParseTrace(trace1Path); Assert.AreEqual("context-options", events[0].Type); Assert.GreaterOrEqual(events.Where(x => x.ApiName == "frame.goto").Count(), 1); Assert.GreaterOrEqual(events.Where(x => x.ApiName == "frame.setContent").Count(), 1); @@ -122,7 +105,7 @@ public async Task ShouldCollectTwoTraces() } { - var events = ParseTrace(trace2Path); + var (events, resources) = ParseTrace(trace2Path); Assert.AreEqual("context-options", events[0].Type); Assert.AreEqual(0, events.Where(x => x.ApiName == "frame.goto").Count()); Assert.AreEqual(0, events.Where(x => x.ApiName == "frame.setContent").Count()); @@ -133,28 +116,59 @@ public async Task ShouldCollectTwoTraces() } - private static IReadOnlyList ParseTrace(string path) + [PlaywrightTest("tracing.spec.ts", "should collect sources")] + public async Task ShouldCollectSources() { - List results = new(); + await Context.Tracing.StartAsync(new() + { + Sources = true, + }); + + var page = await Context.NewPageAsync(); + await page.GotoAsync(Server.Prefix + "/empty.html"); + await page.SetContentAsync(""); + await page.ClickAsync("\"Click\""); + await page.CloseAsync(); + + using var tmp = new TempDirectory(); + var tracePath = Path.Combine(tmp.Path, "trace.zip"); + await Context.Tracing.StopAsync(new() { Path = tracePath }); + + var (events, resources) = ParseTrace(tracePath); + var sourceNames = resources.Keys.Where(key => key.EndsWith(".txt")).ToArray(); + Assert.AreEqual(sourceNames.Count(), 1); + var sourceTraceFileContent = resources[sourceNames[0]]; + var currentFileContent = File.ReadAllText(new StackTrace(true).GetFrame(0).GetFileName()); + + Assert.AreEqual(sourceTraceFileContent, currentFileContent); + } + + private static (IReadOnlyList Events, Dictionary Resources) ParseTrace(string path) + { + Dictionary resources = new(); var archive = ZipFile.OpenRead(path); - foreach (var events in new[] { archive.GetEntry("trace.trace"), archive.GetEntry("trace.network") }) + foreach (var entry in archive.Entries) + { + var memoryStream = new MemoryStream(); + entry.Open().CopyTo(memoryStream); + resources.Add(entry.Name, memoryStream.ToArray()); + } + List events = new(); + foreach (var fileName in new[] { "trace.trace", "trace.network" }) { - if (events != null) + foreach (var line in Encoding.UTF8.GetString(resources[fileName]).Split("\n")) { - var reader = new StreamReader(events.Open()); - while (true) + if (!string.IsNullOrEmpty(line)) { - var line = reader.ReadLine(); - if (line == null) break; - results.Add(JsonSerializer.Deserialize(line, - new JsonSerializerOptions() - { - PropertyNameCaseInsensitive = true, - })); + events.Add(JsonSerializer.Deserialize(line, + new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true, + })); } } } - return results; + return (events, resources); } private class TraceEventEntry diff --git a/src/Playwright/API/Generated/IFrame.cs b/src/Playwright/API/Generated/IFrame.cs index 30f004d9ea..054fc646bf 100644 --- a/src/Playwright/API/Generated/IFrame.cs +++ b/src/Playwright/API/Generated/IFrame.cs @@ -717,7 +717,8 @@ public partial interface IFrame /// A selector to use when resolving DOM element. See working /// with selectors for more details. /// - ILocator Locator(string selector); + /// Call options + ILocator Locator(string selector, FrameLocatorOptions? options = default); /// /// Returns frame's name attribute as specified in the tag. diff --git a/src/Playwright/API/Generated/IFrameLocator.cs b/src/Playwright/API/Generated/IFrameLocator.cs index a167e3f295..cf241ddcaa 100644 --- a/src/Playwright/API/Generated/IFrameLocator.cs +++ b/src/Playwright/API/Generated/IFrameLocator.cs @@ -100,7 +100,8 @@ public partial interface IFrameLocator /// A selector to use when resolving DOM element. See working /// with selectors for more details. /// - ILocator Locator(string selector); + /// Call options + ILocator Locator(string selector, FrameLocatorLocatorOptions? options = default); /// Returns locator to the n-th matching frame. /// diff --git a/src/Playwright/API/Generated/ILocator.cs b/src/Playwright/API/Generated/ILocator.cs index 79fbe15e12..451ba246ca 100644 --- a/src/Playwright/API/Generated/ILocator.cs +++ b/src/Playwright/API/Generated/ILocator.cs @@ -454,7 +454,8 @@ public partial interface ILocator /// A selector to use when resolving DOM element. See working /// with selectors for more details. /// - ILocator Locator(string selector); + /// Call options + ILocator Locator(string selector, LocatorLocatorOptions? options = default); /// Returns locator to the n-th matching element. /// @@ -977,24 +978,6 @@ public partial interface ILocator /// /// Call options Task WaitForAsync(LocatorWaitForOptions? options = default); - - /// - /// - /// Matches elements containing specified text somewhere inside, possibly in a child - /// or a descendant element. For example, "Playwright" matches <article><div>Playwright</div></article>. - /// - /// - /// Text to filter by as a string or as a regular expression. - ILocator WithText(string text); - - /// - /// - /// Matches elements containing specified text somewhere inside, possibly in a child - /// or a descendant element. For example, "Playwright" matches <article><div>Playwright</div></article>. - /// - /// - /// Text to filter by as a string or as a regular expression. - ILocator WithText(Regex text); } } diff --git a/src/Playwright/API/Generated/IPage.cs b/src/Playwright/API/Generated/IPage.cs index f7a208a6ec..d37d116b70 100644 --- a/src/Playwright/API/Generated/IPage.cs +++ b/src/Playwright/API/Generated/IPage.cs @@ -1151,7 +1151,8 @@ public partial interface IPage /// A selector to use when resolving DOM element. See working /// with selectors for more details. /// - ILocator Locator(string selector); + /// Call options + ILocator Locator(string selector, PageLocatorOptions? options = default); /// /// diff --git a/src/Playwright/API/Generated/Options/BrowserNewContextOptions.cs b/src/Playwright/API/Generated/Options/BrowserNewContextOptions.cs index 00bc062cea..b741c0c2cd 100644 --- a/src/Playwright/API/Generated/Options/BrowserNewContextOptions.cs +++ b/src/Playwright/API/Generated/Options/BrowserNewContextOptions.cs @@ -102,6 +102,10 @@ public BrowserNewContextOptions(BrowserNewContextOptions clone) /// baseURL: http://localhost:3000/foo/ and navigating to ./bar.html results /// in http://localhost:3000/foo/bar.html /// + /// + /// baseURL: http://localhost:3000/foo (without trailing slash) and navigating + /// to ./bar.html results in http://localhost:3000/bar.html + /// /// /// [JsonPropertyName("baseURL")] diff --git a/src/Playwright/API/Generated/Options/BrowserNewPageOptions.cs b/src/Playwright/API/Generated/Options/BrowserNewPageOptions.cs index 32801aa2a7..370ac26d22 100644 --- a/src/Playwright/API/Generated/Options/BrowserNewPageOptions.cs +++ b/src/Playwright/API/Generated/Options/BrowserNewPageOptions.cs @@ -102,6 +102,10 @@ public BrowserNewPageOptions(BrowserNewPageOptions clone) /// baseURL: http://localhost:3000/foo/ and navigating to ./bar.html results /// in http://localhost:3000/foo/bar.html /// + /// + /// baseURL: http://localhost:3000/foo (without trailing slash) and navigating + /// to ./bar.html results in http://localhost:3000/bar.html + /// /// /// [JsonPropertyName("baseURL")] diff --git a/src/Playwright/API/Generated/Options/BrowserTypeLaunchPersistentContextOptions.cs b/src/Playwright/API/Generated/Options/BrowserTypeLaunchPersistentContextOptions.cs index 1aeb327ce2..9139e74f8b 100644 --- a/src/Playwright/API/Generated/Options/BrowserTypeLaunchPersistentContextOptions.cs +++ b/src/Playwright/API/Generated/Options/BrowserTypeLaunchPersistentContextOptions.cs @@ -125,6 +125,10 @@ public BrowserTypeLaunchPersistentContextOptions(BrowserTypeLaunchPersistentCont /// baseURL: http://localhost:3000/foo/ and navigating to ./bar.html results /// in http://localhost:3000/foo/bar.html /// + /// + /// baseURL: http://localhost:3000/foo (without trailing slash) and navigating + /// to ./bar.html results in http://localhost:3000/bar.html + /// /// /// [JsonPropertyName("baseURL")] diff --git a/src/Playwright/API/Generated/Options/FrameLocatorLocatorOptions.cs b/src/Playwright/API/Generated/Options/FrameLocatorLocatorOptions.cs new file mode 100644 index 0000000000..1a2f8a0639 --- /dev/null +++ b/src/Playwright/API/Generated/Options/FrameLocatorLocatorOptions.cs @@ -0,0 +1,73 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace Microsoft.Playwright +{ + public class FrameLocatorLocatorOptions + { + public FrameLocatorLocatorOptions() { } + + public FrameLocatorLocatorOptions(FrameLocatorLocatorOptions clone) + { + if (clone == null) return; + HasTextString = clone.HasTextString; + HasTextRegex = clone.HasTextRegex; + } + + /// + /// + /// Matches elements containing specified text somewhere inside, possibly in a child + /// or a descendant element. For example, "Playwright" matches <article><div>Playwright</div></article>. + /// + /// + [JsonPropertyName("hasTextString")] + public string? HasTextString { get; set; } + + /// + /// + /// Matches elements containing specified text somewhere inside, possibly in a child + /// or a descendant element. For example, "Playwright" matches <article><div>Playwright</div></article>. + /// + /// + [JsonPropertyName("hasTextRegex")] + public Regex? HasTextRegex { get; set; } + } +} + +#nullable disable diff --git a/src/Playwright/API/Generated/Options/FrameLocatorOptions.cs b/src/Playwright/API/Generated/Options/FrameLocatorOptions.cs new file mode 100644 index 0000000000..911a1bb4fc --- /dev/null +++ b/src/Playwright/API/Generated/Options/FrameLocatorOptions.cs @@ -0,0 +1,73 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace Microsoft.Playwright +{ + public class FrameLocatorOptions + { + public FrameLocatorOptions() { } + + public FrameLocatorOptions(FrameLocatorOptions clone) + { + if (clone == null) return; + HasTextString = clone.HasTextString; + HasTextRegex = clone.HasTextRegex; + } + + /// + /// + /// Matches elements containing specified text somewhere inside, possibly in a child + /// or a descendant element. For example, "Playwright" matches <article><div>Playwright</div></article>. + /// + /// + [JsonPropertyName("hasTextString")] + public string? HasTextString { get; set; } + + /// + /// + /// Matches elements containing specified text somewhere inside, possibly in a child + /// or a descendant element. For example, "Playwright" matches <article><div>Playwright</div></article>. + /// + /// + [JsonPropertyName("hasTextRegex")] + public Regex? HasTextRegex { get; set; } + } +} + +#nullable disable diff --git a/src/Playwright/API/Generated/Options/LocatorLocatorOptions.cs b/src/Playwright/API/Generated/Options/LocatorLocatorOptions.cs new file mode 100644 index 0000000000..95d3a9be03 --- /dev/null +++ b/src/Playwright/API/Generated/Options/LocatorLocatorOptions.cs @@ -0,0 +1,73 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace Microsoft.Playwright +{ + public class LocatorLocatorOptions + { + public LocatorLocatorOptions() { } + + public LocatorLocatorOptions(LocatorLocatorOptions clone) + { + if (clone == null) return; + HasTextString = clone.HasTextString; + HasTextRegex = clone.HasTextRegex; + } + + /// + /// + /// Matches elements containing specified text somewhere inside, possibly in a child + /// or a descendant element. For example, "Playwright" matches <article><div>Playwright</div></article>. + /// + /// + [JsonPropertyName("hasTextString")] + public string? HasTextString { get; set; } + + /// + /// + /// Matches elements containing specified text somewhere inside, possibly in a child + /// or a descendant element. For example, "Playwright" matches <article><div>Playwright</div></article>. + /// + /// + [JsonPropertyName("hasTextRegex")] + public Regex? HasTextRegex { get; set; } + } +} + +#nullable disable diff --git a/src/Playwright/API/Generated/Options/PageLocatorOptions.cs b/src/Playwright/API/Generated/Options/PageLocatorOptions.cs new file mode 100644 index 0000000000..22f473dbe1 --- /dev/null +++ b/src/Playwright/API/Generated/Options/PageLocatorOptions.cs @@ -0,0 +1,73 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace Microsoft.Playwright +{ + public class PageLocatorOptions + { + public PageLocatorOptions() { } + + public PageLocatorOptions(PageLocatorOptions clone) + { + if (clone == null) return; + HasTextString = clone.HasTextString; + HasTextRegex = clone.HasTextRegex; + } + + /// + /// + /// Matches elements containing specified text somewhere inside, possibly in a child + /// or a descendant element. For example, "Playwright" matches <article><div>Playwright</div></article>. + /// + /// + [JsonPropertyName("hasTextString")] + public string? HasTextString { get; set; } + + /// + /// + /// Matches elements containing specified text somewhere inside, possibly in a child + /// or a descendant element. For example, "Playwright" matches <article><div>Playwright</div></article>. + /// + /// + [JsonPropertyName("hasTextRegex")] + public Regex? HasTextRegex { get; set; } + } +} + +#nullable disable diff --git a/src/Playwright/API/Generated/Options/TracingStartOptions.cs b/src/Playwright/API/Generated/Options/TracingStartOptions.cs index 54534e76d4..943a2ccb34 100644 --- a/src/Playwright/API/Generated/Options/TracingStartOptions.cs +++ b/src/Playwright/API/Generated/Options/TracingStartOptions.cs @@ -49,6 +49,7 @@ public TracingStartOptions(TracingStartOptions clone) Name = clone.Name; Screenshots = clone.Screenshots; Snapshots = clone.Snapshots; + Sources = clone.Sources; Title = clone.Title; } @@ -74,6 +75,10 @@ public TracingStartOptions(TracingStartOptions clone) [JsonPropertyName("snapshots")] public bool? Snapshots { get; set; } + /// Whether to include source files for trace actions. + [JsonPropertyName("sources")] + public bool? Sources { get; set; } + /// Trace name to be shown in the Trace Viewer. [JsonPropertyName("title")] public string? Title { get; set; } diff --git a/src/Playwright/API/Supplements/LocalUtils.cs b/src/Playwright/API/Supplements/LocalUtils.cs new file mode 100644 index 0000000000..93de2803bd --- /dev/null +++ b/src/Playwright/API/Supplements/LocalUtils.cs @@ -0,0 +1,33 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System.Text.Json; +using System.Threading.Tasks; + +namespace Microsoft.Playwright +{ + public partial interface ILocalUtils + { + } +} diff --git a/src/Playwright/Core/Browser.cs b/src/Playwright/Core/Browser.cs index 4af651c331..ee75640644 100644 --- a/src/Playwright/Core/Browser.cs +++ b/src/Playwright/Core/Browser.cs @@ -62,6 +62,8 @@ internal Browser(IChannelOwner parent, string guid, BrowserInitializer initializ internal List BrowserContextsList { get; } = new(); + internal LocalUtils LocalUtils { get; set; } + public async Task CloseAsync() { try @@ -116,6 +118,7 @@ public async Task NewContextAsync(BrowserNewContextOptions opti forcedColors: options.ForcedColors).ConfigureAwait(false)).Object; context.Options = options; + context.LocalUtils = LocalUtils; BrowserContextsList.Add(context); return context; diff --git a/src/Playwright/Core/BrowserContext.cs b/src/Playwright/Core/BrowserContext.cs index 81f83a46ec..716454d211 100644 --- a/src/Playwright/Core/BrowserContext.cs +++ b/src/Playwright/Core/BrowserContext.cs @@ -83,7 +83,7 @@ internal BrowserContext(IChannelOwner parent, string guid, BrowserContextInitial e.Page?.FireResponse(e.Response); }; - _tracing = new Tracing(Channel); + _tracing = new Tracing(this); _initializer = initializer; Browser = parent as IBrowser; } @@ -112,6 +112,8 @@ public ITracing Tracing public IBrowser Browser { get; } + internal LocalUtils LocalUtils { get; set; } + public IReadOnlyList Pages => PagesList; internal float DefaultNavigationTimeout @@ -293,8 +295,8 @@ public async Task InnerWaitForEventAsync(PlaywrightEvent playwrightEven } timeout ??= DefaultTimeout; - using var waiter = new Waiter(Channel, $"context.WaitForEventAsync(\"{playwrightEvent.Name}\")"); - waiter.RejectOnTimeout(Convert.ToInt32(timeout), $"Timeout while waiting for event \"{playwrightEvent.Name}\""); + using var waiter = new Waiter(this, $"context.WaitForEventAsync(\"{playwrightEvent.Name}\")"); + waiter.RejectOnTimeout(Convert.ToInt32(timeout), $"Timeout {timeout}ms exceeded while waiting for event \"{playwrightEvent.Name}\""); if (playwrightEvent.Name != BrowserContextEvent.Close.Name) { diff --git a/src/Playwright/Core/BrowserType.cs b/src/Playwright/Core/BrowserType.cs index 1b309d64c1..726b63113a 100644 --- a/src/Playwright/Core/BrowserType.cs +++ b/src/Playwright/Core/BrowserType.cs @@ -47,6 +47,8 @@ internal BrowserType(IChannelOwner parent, string guid, BrowserTypeInitializer i IChannel IChannelOwner.Channel => _channel; + internal PlaywrightImpl Playwright { get; set; } + public string ExecutablePath => _initializer.ExecutablePath; public string Name => _initializer.Name; @@ -54,7 +56,7 @@ internal BrowserType(IChannelOwner parent, string guid, BrowserTypeInitializer i public async Task LaunchAsync(BrowserTypeLaunchOptions options = default) { options ??= new BrowserTypeLaunchOptions(); - return (await _channel.LaunchAsync( + Browser browser = (await _channel.LaunchAsync( headless: options.Headless, channel: options.Channel, executablePath: options.ExecutablePath, @@ -73,6 +75,8 @@ public async Task LaunchAsync(BrowserTypeLaunchOptions options = defau slowMo: options.SlowMo, ignoreDefaultArgs: options.IgnoreDefaultArgs, ignoreAllDefaultArgs: options.IgnoreAllDefaultArgs).ConfigureAwait(false)).Object; + browser.LocalUtils = Playwright.Utils; + return browser; } public async Task LaunchPersistentContextAsync(string userDataDir, BrowserTypeLaunchPersistentContextOptions options = default) @@ -130,6 +134,7 @@ public async Task LaunchPersistentContextAsync(string userDataD RecordHarPath = options.RecordHarPath, RecordHarOmitContent = options.RecordHarOmitContent, }; + context.LocalUtils = Playwright.Utils; return context; } @@ -159,7 +164,7 @@ void ClosePipe() void OnPipeClosed() { // Emulate all pages, contexts and the browser closing upon disconnect. - foreach (BrowserContext context in browser.BrowserContextsList.ToArray()) + foreach (BrowserContext context in browser?.BrowserContextsList.ToArray() ?? Array.Empty()) { foreach (Page page in context.PagesList.ToArray()) { @@ -167,7 +172,7 @@ void OnPipeClosed() } context.OnClose(); } - browser.DidClose(); + browser?.DidClose(); connection.DoClose(closeError != null ? closeError : DriverMessages.BrowserClosedExceptionMessage); } pipe.Closed += (_, _) => OnPipeClosed(); @@ -192,6 +197,7 @@ void OnPipeClosed() catch (Exception ex) { closeError = ex.ToString(); + _channel.Connection.TraceMessage("pw:dotnet", $"Dispatching error: {ex.Message}\n{ex.StackTrace}"); ClosePipe(); } }; @@ -208,6 +214,7 @@ async Task CreateBrowserAsync() browser = playwright.PreLaunchedBrowser; browser.ShouldCloseConnectionOnClose = true; browser.Disconnected += (_, _) => ClosePipe(); + browser.LocalUtils = Playwright.Utils; return playwright.PreLaunchedBrowser; } var task = CreateBrowserAsync(); @@ -228,6 +235,7 @@ public async Task ConnectOverCDPAsync(string endpointURL, BrowserTypeC { browser.BrowserContextsList.Add(defaultContextValue.ToObject(_channel.Connection.GetDefaultJsonSerializerOptions())); } + browser.LocalUtils = Playwright.Utils; return browser; } } diff --git a/src/Playwright/Core/Frame.cs b/src/Playwright/Core/Frame.cs index d8e1b5f7d3..936ded4d9e 100644 --- a/src/Playwright/Core/Frame.cs +++ b/src/Playwright/Core/Frame.cs @@ -276,6 +276,9 @@ public Task TapAsync(string selector, FrameTapOptions options = default) trial: options?.Trial, strict: options?.Strict); + internal Task QueryCountAsync(string selector) + => _channel.QueryCountAsync(selector); + public Task ContentAsync() => _channel.ContentAsync(); public Task FocusAsync(string selector, FrameFocusOptions options = default) @@ -517,7 +520,7 @@ public async Task EvalOnSelectorAllAsync(string selector, string script, o script, arg: ScriptsHelper.SerializedArgument(arg)).ConfigureAwait(false)); - public ILocator Locator(string selector) => new Locator(this, selector); + public ILocator Locator(string selector, FrameLocatorOptions options = null) => new Locator(this, selector, new() { HasTextRegex = options?.HasTextRegex, HasTextString = options?.HasTextString }); public async Task QuerySelectorAsync(string selector, FrameQuerySelectorOptions options = null) => (await _channel.QuerySelectorAsync(selector, options?.Strict).ConfigureAwait(false))?.Object; @@ -597,7 +600,7 @@ private Task WaitForURLAsync(string urlString, Regex urlRegex, Func new FrameLocator(_frame, $"{_frameSelector} >> control=enter-frame >> {selector}"); - ILocator IFrameLocator.Locator(string selector) => new Locator(_frame, $"{_frameSelector} >> control=enter-frame >> {selector}"); + ILocator IFrameLocator.Locator(string selector, FrameLocatorLocatorOptions options) => new Locator(_frame, $"{_frameSelector} >> control=enter-frame >> {selector}", new() { HasTextRegex = options?.HasTextRegex, HasTextString = options?.HasTextString }); IFrameLocator IFrameLocator.Nth(int index) => new FrameLocator(_frame, $"{_frameSelector} >> nth={index}"); } diff --git a/src/Playwright/Core/LocalUtils.cs b/src/Playwright/Core/LocalUtils.cs index 9e209e1c7d..9918ea9ff4 100644 --- a/src/Playwright/Core/LocalUtils.cs +++ b/src/Playwright/Core/LocalUtils.cs @@ -22,15 +22,31 @@ * SOFTWARE. */ +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; +using Microsoft.Playwright.Helpers; +using Microsoft.Playwright.Transport; using Microsoft.Playwright.Transport.Channels; +using Microsoft.Playwright.Transport.Protocol; namespace Microsoft.Playwright.Core { - internal class LocalUtils + internal partial class LocalUtils : ChannelOwnerBase, IChannelOwner, ILocalUtils { - public LocalUtils(IChannelOwner parent) + private readonly LocalUtilsChannel _channel; + + public LocalUtils(IChannelOwner parent, string guid, JsonElement? initializer) : base(parent, guid) { + _channel = new(guid, parent.Connection, this); } + + ChannelBase IChannelOwner.Channel => _channel; + + IChannel IChannelOwner.Channel => _channel; + + internal Task ZipAsync(string zipFile, List entries) + => _channel.ZipAsync(zipFile, entries); } } diff --git a/src/Playwright/Core/Locator.cs b/src/Playwright/Core/Locator.cs index 1a154dc5ce..d53eaf8cc5 100644 --- a/src/Playwright/Core/Locator.cs +++ b/src/Playwright/Core/Locator.cs @@ -39,10 +39,22 @@ internal class Locator : ILocator private readonly Frame _frame; private readonly string _selector; - public Locator(Frame parent, string selector) + private readonly LocatorLocatorOptions _options; + + public Locator(Frame parent, string selector, LocatorLocatorOptions options = null) { _frame = parent; _selector = selector; + _options = options; + + if (options?.HasTextRegex != null) + { + _selector += $" >> :scope:text-matches({options.HasTextRegex.ToString().EscapeWithQuotes("\"")}, {options.HasTextRegex.Options.GetInlineFlags().EscapeWithQuotes("\"")})"; + } + if (options?.HasTextString != null) + { + _selector += $" >> :scope:has-text({options.HasTextString.EscapeWithQuotes("\"")})"; + } } public ILocator First => new Locator(_frame, $"{_selector} >> nth=0"); @@ -104,7 +116,7 @@ public Task SetCheckedAsync(bool checkedState, LocatorSetCheckedOptions options : UncheckAsync(ConvertOptions(options)); public Task CountAsync() - => EvaluateAllAsync("ee => ee.length"); + => _frame.QueryCountAsync(_selector); public Task DblClickAsync(LocatorDblClickOptions options = null) => _frame.DblClickAsync(_selector, ConvertOptions(options)); @@ -180,12 +192,6 @@ public Task IsVisibleAsync(LocatorIsVisibleOptions options = null) public ILocator Nth(int index) => new Locator(_frame, $"{_selector} >> nth={index}"); - public ILocator WithText(string text) - => new Locator(_frame, $"{_selector} >> :scope:has-text({text.EscapeWithQuotes("\"")})"); - - public ILocator WithText(Regex regex) - => new Locator(_frame, $"{_selector} >> :scope:text-matches({regex.ToString().EscapeWithQuotes("\"")}, {regex.Options.GetInlineFlags().EscapeWithQuotes("\"")})"); - public Task PressAsync(string key, LocatorPressOptions options = null) => _frame.PressAsync(_selector, key, ConvertOptions(options)); @@ -240,8 +246,8 @@ public Task TypeAsync(string text, LocatorTypeOptions options = null) public Task UncheckAsync(LocatorUncheckOptions options = null) => _frame.UncheckAsync(_selector, ConvertOptions(options)); - ILocator ILocator.Locator(string selector) - => new Locator(_frame, $"{_selector} >> {selector}"); + ILocator ILocator.Locator(string selector, LocatorLocatorOptions options) + => new Locator(_frame, $"{_selector} >> {selector}", options); public Task WaitForAsync(LocatorWaitForOptions options = null) => _frame.LocatorWaitForAsync(_selector, ConvertOptions(options)); diff --git a/src/Playwright/Core/Page.cs b/src/Playwright/Core/Page.cs index 76fb419048..5c25bc4ea8 100644 --- a/src/Playwright/Core/Page.cs +++ b/src/Playwright/Core/Page.cs @@ -423,8 +423,8 @@ public async Task InnerWaitForEventAsync(PlaywrightEvent pageEvent, Fun } timeout ??= _defaultTimeout; - using var waiter = new Waiter(_channel, $"page.WaitForEventAsync(\"{typeof(T)}\")"); - waiter.RejectOnTimeout(Convert.ToInt32(timeout), $"Timeout while waiting for event \"{pageEvent.Name}\""); + using var waiter = new Waiter(this, $"page.WaitForEventAsync(\"{typeof(T)}\")"); + waiter.RejectOnTimeout(Convert.ToInt32(timeout), $"Timeout {timeout}ms exceeded while waiting for event \"{pageEvent.Name}\""); if (pageEvent.Name != PageEvent.Crash.Name) { @@ -468,8 +468,8 @@ public async Task CloseAsync(PageCloseOptions options = default) public Task EvalOnSelectorAsync(string selector, string expression, object arg = null, PageEvalOnSelectorOptions options = null) => MainFrame.EvalOnSelectorAsync(selector, expression, arg, new() { Strict = options?.Strict }); - public ILocator Locator(string selector) - => MainFrame.Locator(selector); + public ILocator Locator(string selector, PageLocatorOptions options = default) + => MainFrame.Locator(selector, new() { HasTextString = options?.HasTextString, HasTextRegex = options?.HasTextRegex }); public Task QuerySelectorAsync(string selector, PageQuerySelectorOptions options = null) => MainFrame.QuerySelectorAsync(selector, new() { Strict = options?.Strict }); diff --git a/src/Playwright/Core/PlaywrightImpl.cs b/src/Playwright/Core/PlaywrightImpl.cs index 09bb18bfff..ca08d2c13f 100644 --- a/src/Playwright/Core/PlaywrightImpl.cs +++ b/src/Playwright/Core/PlaywrightImpl.cs @@ -55,6 +55,11 @@ internal PlaywrightImpl(IChannelOwner parent, string guid, PlaywrightInitializer { _devices[entry.Name] = entry.Descriptor; } + Utils = initializer.Utils; + + _initializer.Chromium.Playwright = this; + _initializer.Firefox.Playwright = this; + _initializer.Webkit.Playwright = this; } ~PlaywrightImpl() => Dispose(false); @@ -79,6 +84,8 @@ internal PlaywrightImpl(IChannelOwner parent, string guid, PlaywrightInitializer internal Browser PreLaunchedBrowser => _initializer.PreLaunchedBrowser; + internal LocalUtils Utils { get; set; } + /// /// Gets a . /// diff --git a/src/Playwright/Core/Tracing.cs b/src/Playwright/Core/Tracing.cs index abc27974a7..bde6b76a95 100644 --- a/src/Playwright/Core/Tracing.cs +++ b/src/Playwright/Core/Tracing.cs @@ -23,50 +23,74 @@ */ using System.Threading.Tasks; -using Microsoft.Playwright.Transport.Channels; namespace Microsoft.Playwright.Core { internal partial class Tracing : ITracing { - private readonly BrowserContextChannel _channel; + private readonly BrowserContext _context; - public Tracing(BrowserContextChannel channel) + public Tracing(BrowserContext context) { - _channel = channel; + _context = context; } public async Task StartAsync(TracingStartOptions options = default) { - await _channel.TracingStartAsync( + await _context.Channel.TracingStartAsync( name: options?.Name, + title: options?.Title, screenshots: options?.Screenshots, - snapshots: options?.Snapshots).ConfigureAwait(false); - - await StartChunkAsync().ConfigureAwait(false); + snapshots: options?.Snapshots, + sources: options?.Sources).ConfigureAwait(false); + await _context.Channel.StartChunkAsync(options?.Title).ConfigureAwait(false); } - public Task StartChunkAsync() => StartChunkAsync(default); + public Task StartChunkAsync() => StartChunkAsync(); - public Task StartChunkAsync(TracingStartChunkOptions options) => _channel.StartChunkAsync(options?.Title); + public Task StartChunkAsync(TracingStartChunkOptions options) => _context.Channel.StartChunkAsync(title: options?.Title); public async Task StopChunkAsync(TracingStopChunkOptions options = null) { - var artifact = await _channel.StopChunkAsync(!string.IsNullOrEmpty(options?.Path)).ConfigureAwait(false); - if (artifact == null) - return; - - if (string.IsNullOrEmpty(options?.Path)) - throw new PlaywrightException("Specified path was invalid or empty. Trace could not be saved."); - - await artifact.SaveAsAsync(options.Path).ConfigureAwait(false); - await artifact.DeleteAsync().ConfigureAwait(false); + await DoStopChunkAsync(filePath: options.Path).ConfigureAwait(false); } public async Task StopAsync(TracingStopOptions options = default) { await StopChunkAsync(new() { Path = options?.Path }).ConfigureAwait(false); - await _channel.TracingStopAsync().ConfigureAwait(false); + await _context.Channel.TracingStopAsync().ConfigureAwait(false); + } + + private async Task DoStopChunkAsync(string filePath) + { + bool isLocal = _context.Channel.Connection.IsRemote; + + var mode = "doNotSave"; + if (!string.IsNullOrEmpty(filePath)) + { + if (isLocal) + mode = "compressTraceAndSources"; + else + mode = "compressTrace"; + } + + var (artifact, sourceEntries) = await _context.Channel.StopChunkAsync(mode).ConfigureAwait(false); + + // Not interested in artifacts. + if (string.IsNullOrEmpty(filePath)) + throw new PlaywrightException("Specified path was invalid or empty. Trace could not be saved."); + + // The artifact may be missing if the browser closed while stopping tracing. + if (artifact == null) + return; + + // Save trace to the final local file. + await artifact.SaveAsAsync(filePath).ConfigureAwait(false); + await artifact.DeleteAsync().ConfigureAwait(false); + + // Add local sources to the remote trace if necessary. + if (sourceEntries.Count > 0) + await _context.LocalUtils.ZipAsync(filePath, sourceEntries).ConfigureAwait(false); } } } diff --git a/src/Playwright/Core/Waiter.cs b/src/Playwright/Core/Waiter.cs index 7732095de7..6431519e85 100644 --- a/src/Playwright/Core/Waiter.cs +++ b/src/Playwright/Core/Waiter.cs @@ -39,24 +39,23 @@ internal class Waiter : IDisposable private readonly List _dispose = new(); private readonly CancellationTokenSource _cts = new(); private readonly string _waitId = Guid.NewGuid().ToString(); - private readonly ChannelBase _channel; + private readonly IChannelOwner _channelOwner; private Exception _immediateError; private bool _disposed; private string _error; - internal Waiter(ChannelBase channel, string @event) + internal Waiter(IChannelOwner channelOwner, string @event) { - _channel = channel; + _channelOwner = channelOwner; var beforeArgs = new { info = new { @event = @event, waitId = _waitId, phase = "before" } }; - var connection = _channel.Connection; - _ = connection.SendMessageToServerAsync(channel.Guid, "waitForEventInfo", beforeArgs); + _ = channelOwner.Connection.SendMessageToServerAsync(channelOwner.Channel.Guid, "waitForEventInfo", beforeArgs); _dispose.Add(() => { var afterArgs = new { info = new { waitId = _waitId, phase = "after", error = _error } }; - _ = _channel.Connection.SendMessageToServerAsync(channel.Guid, "waitForEventInfo", afterArgs); + _ = channelOwner.Connection.SendMessageToServerAsync(channelOwner.Channel.Guid, "waitForEventInfo", afterArgs); }); } @@ -79,7 +78,7 @@ internal void Log(string log) { _logs.Add(log); var logArgs = new { info = new { waitId = _waitId, phase = "log", message = log } }; - _ = _channel.Connection.SendMessageToServerAsync(_channel.Guid, "waitForEventInfo", logArgs); + _ = _channelOwner.Connection.SendMessageToServerAsync(_channelOwner.Channel.Guid, "waitForEventInfo", logArgs); } internal void RejectImmediately(Exception exception) diff --git a/src/Playwright/Core/Worker.cs b/src/Playwright/Core/Worker.cs index 2b35849cd8..2f218b4285 100644 --- a/src/Playwright/Core/Worker.cs +++ b/src/Playwright/Core/Worker.cs @@ -84,7 +84,7 @@ public async Task EvaluateHandleAsync(string expression, object arg = public async Task WaitForCloseAsync(Func action = default, float? timeout = default) { - using var waiter = new Waiter(_channel, "worker.WaitForCloseAsync"); + using var waiter = new Waiter(this, "worker.WaitForCloseAsync"); var waiterResult = waiter.GetWaitForEventTask(this, nameof(Close), null); var result = waiterResult.Task.WithTimeout(Convert.ToInt32(timeout ?? 0)); if (action != null) diff --git a/src/Playwright/Helpers/StringExtensions.cs b/src/Playwright/Helpers/StringExtensions.cs index f2072b9fd9..1c39ae14bc 100644 --- a/src/Playwright/Helpers/StringExtensions.cs +++ b/src/Playwright/Helpers/StringExtensions.cs @@ -624,7 +624,7 @@ public static Dictionary ParseQueryString(this string query) query = query.Substring(1, query.Length - 1); } - foreach (string keyValue in query.Split('&').Where(kv => kv.Contains("="))) + foreach (string keyValue in query.Split('&').Where(kv => kv.Contains('='))) { string[] pair = keyValue.Split('='); result[pair[0]] = pair[1]; diff --git a/src/Playwright/Transport/Channels/BrowserContextChannel.cs b/src/Playwright/Transport/Channels/BrowserContextChannel.cs index 22864780e3..7b28b331c5 100644 --- a/src/Playwright/Transport/Channels/BrowserContextChannel.cs +++ b/src/Playwright/Transport/Channels/BrowserContextChannel.cs @@ -234,15 +234,17 @@ internal Task SetExtraHTTPHeadersAsync(IEnumerable> internal Task GetStorageStateAsync() => Connection.SendMessageToServerAsync(Guid, "storageState", null); - internal Task TracingStartAsync(string name, bool? screenshots, bool? snapshots) + internal Task TracingStartAsync(string name, string title, bool? screenshots, bool? snapshots, bool? sources) => Connection.SendMessageToServerAsync( Guid, "tracingStart", new Dictionary { ["name"] = name, + ["title"] = title, ["screenshots"] = screenshots, ["snapshots"] = snapshots, + ["sources"] = sources, }); internal Task TracingStopAsync() @@ -256,18 +258,28 @@ internal Task StartChunkAsync(string title = null) ["title"] = title, }); - internal async Task StopChunkAsync(bool save = false, bool skipCompress = false) + internal async Task<(Artifact Artifact, List SourceEntries)> StopChunkAsync(string mode) { var result = await Connection.SendMessageToServerAsync(Guid, "tracingStopChunk", new Dictionary { - ["save"] = save, - ["skipCompress"] = skipCompress, + ["mode"] = mode, }).ConfigureAwait(false); - if (save) - return result.GetObject("artifact", Connection); - - return null; + var artifact = result.GetObject("artifact", Connection); + List sourceEntries = new() { }; + if (result.Value.TryGetProperty("sourceEntries", out var sourceEntriesElement)) + { + var sourceEntriesEnumerator = sourceEntriesElement.EnumerateArray(); + while (sourceEntriesEnumerator.MoveNext()) + { + JsonElement current = sourceEntriesEnumerator.Current; + sourceEntries.Add(current.Deserialize(new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true, + })); + } + } + return (artifact, sourceEntries); } internal async Task HarExportAsync() diff --git a/src/Playwright/Transport/Channels/ChannelOwnerType.cs b/src/Playwright/Transport/Channels/ChannelOwnerType.cs index 98828ac6ad..94c0a08416 100644 --- a/src/Playwright/Transport/Channels/ChannelOwnerType.cs +++ b/src/Playwright/Transport/Channels/ChannelOwnerType.cs @@ -63,6 +63,8 @@ internal enum ChannelOwnerType [EnumMember(Value = "JsonPipe")] JsonPipe, + [EnumMember(Value = "LocalUtils")] + LocalUtils, [EnumMember(Value = "page")] Page, diff --git a/src/Playwright/Transport/Channels/ElementHandleChannel.cs b/src/Playwright/Transport/Channels/ElementHandleChannel.cs index a00647d57b..2ec30833b2 100644 --- a/src/Playwright/Transport/Channels/ElementHandleChannel.cs +++ b/src/Playwright/Transport/Channels/ElementHandleChannel.cs @@ -271,7 +271,7 @@ internal Task SetInputFilesAsync(IEnumerable files, bool? noWaitAft ["noWaitAfter"] = noWaitAfter, }; - return Connection.SendMessageToServerAsync(Guid, "setInputFiles", args); + return Connection.SendMessageToServerAsync(Guid, "setInputFiles", args); } internal async Task GetAttributeAsync(string name) diff --git a/src/Playwright/Transport/Channels/FrameChannel.cs b/src/Playwright/Transport/Channels/FrameChannel.cs index 30d8ac09df..9d11ba150c 100644 --- a/src/Playwright/Transport/Channels/FrameChannel.cs +++ b/src/Playwright/Transport/Channels/FrameChannel.cs @@ -239,6 +239,17 @@ internal Task WaitForLoadStateAsync(LoadState? state, float? timeout) param); } + internal async Task QueryCountAsync(string selector) + { + var args = new Dictionary + { + ["selector"] = selector, + }; + + var result = await Connection.SendMessageToServerAsync(Guid, "queryCount", args).ConfigureAwait(false); + return result.Value.GetProperty("value").GetInt32(); + } + internal Task SetContentAsync(string html, float? timeout, WaitUntilState? waitUntil) { var args = new Dictionary @@ -413,7 +424,7 @@ internal Task HoverAsync( return Connection.SendMessageToServerAsync(Guid, "hover", args); } - internal Task PressAsync(string selector, string text, float? delay, float? timeout, bool? noWaitAfter, bool? strict) + internal Task PressAsync(string selector, string text, float? delay, float? timeout, bool? noWaitAfter, bool? strict) { var args = new Dictionary { @@ -425,7 +436,7 @@ internal Task PressAsync(string selector, string text, float? delay, f ["strict"] = strict, }; - return Connection.SendMessageToServerAsync(Guid, "press", args); + return Connection.SendMessageToServerAsync(Guid, "press", args); } internal async Task SelectOptionAsync(string selector, IEnumerable values, bool? noWaitAfter, bool? strict, bool? force, float? timeout) @@ -550,7 +561,7 @@ internal Task SetInputFilesAsync(string selector, IEnumerable files ["strict"] = strict, }; - return Connection.SendMessageToServerAsync(Guid, "setInputFiles", args); + return Connection.SendMessageToServerAsync(Guid, "setInputFiles", args); } internal async Task TextContentAsync(string selector, float? timeout, bool? strict) diff --git a/src/Playwright/Transport/Channels/LocalUtilsChannel.cs b/src/Playwright/Transport/Channels/LocalUtilsChannel.cs new file mode 100644 index 0000000000..f0c674dda2 --- /dev/null +++ b/src/Playwright/Transport/Channels/LocalUtilsChannel.cs @@ -0,0 +1,46 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and / or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Playwright.Core; + +namespace Microsoft.Playwright.Transport.Channels +{ + internal class LocalUtilsChannel : Channel + { + public LocalUtilsChannel(string guid, Connection connection, LocalUtils owner) : base(guid, connection, owner) + { + } + + internal Task ZipAsync(string zipFile, List entries) => + Connection.SendMessageToServerAsync(Guid, "zip", new Dictionary + { + { "zipFile", zipFile }, + { "entries", entries }, + }); + } +} diff --git a/src/Playwright/Transport/Connection.cs b/src/Playwright/Transport/Connection.cs index e36366741d..b872b68409 100644 --- a/src/Playwright/Transport/Connection.cs +++ b/src/Playwright/Transport/Connection.cs @@ -269,7 +269,7 @@ private void CreateRemoteObject(string parentGuid, ChannelOwnerType type, string IChannelOwner result = null; var parent = string.IsNullOrEmpty(parentGuid) ? _rootObject : Objects[parentGuid]; -#pragma warning disable CA2000 +#pragma warning disable CA2000 // Dispose objects before losing scope switch (type) { case ChannelOwnerType.Artifact: @@ -311,6 +311,9 @@ private void CreateRemoteObject(string parentGuid, ChannelOwnerType type, string case ChannelOwnerType.JsonPipe: result = new JsonPipe(parent, guid, initializer?.ToObject(GetDefaultJsonSerializerOptions())); break; + case ChannelOwnerType.LocalUtils: + result = new LocalUtils(parent, guid, initializer); + break; case ChannelOwnerType.Page: result = new Page(parent, guid, initializer?.ToObject(GetDefaultJsonSerializerOptions())); break; @@ -339,15 +342,15 @@ private void CreateRemoteObject(string parentGuid, ChannelOwnerType type, string TraceMessage("pw:dotnet", "Missing type " + type); break; } +#pragma warning restore CA2000 Objects.TryAdd(guid, result); OnObjectCreated(guid, result); -#pragma warning restore CA2000 } private void DoClose(Exception ex) { - TraceMessage("pw:dotnet", ex.ToString()); + TraceMessage("pw:dotnet", $"Connection Close: {ex.Message}\n{ex.StackTrace}"); DoClose(ex.Message); } @@ -416,10 +419,15 @@ private void Dispose(bool disposing) } [Conditional("DEBUG")] - private void TraceMessage(string logLevel, object message) + internal void TraceMessage(string logLevel, object message) { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DEBUGPWD"))) + string actualLogLevel = Environment.GetEnvironmentVariable("DEBUG"); + if (!string.IsNullOrEmpty(actualLogLevel)) { + if (!actualLogLevel.Contains(logLevel)) + { + return; + } if (!(message is string)) { message = JsonSerializer.Serialize(message, GetDefaultJsonSerializerOptions());