Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/Playwright.Tests/ExtensionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,30 @@ public void ShouldSerializeRegexpFlagsCorrectly()
Assert.AreEqual(new Regex("foo", RegexOptions.IgnorePatternWhitespace).Options.GetInlineFlags(), "ism");
});
}

[Test]
public void ShouldExtractLeadingInlineFlags()
{
Assert.AreEqual(("foo", ""), new Regex("foo").GetSourceAndFlags());
Assert.AreEqual((".+\\.css$", "i"), new Regex(@"(?i).+\.css$").GetSourceAndFlags());
Assert.AreEqual(("bar", "im"), new Regex("(?im)bar").GetSourceAndFlags());
Assert.AreEqual(("bar", "ism"), new Regex("(?ims)bar").GetSourceAndFlags());

// Constructor flags merge with inline flags.
Assert.AreEqual(("bar", "im"), new Regex("(?i)bar", RegexOptions.Multiline).GetSourceAndFlags());

// Disable form: (?-i) clears the constructor flag.
Assert.AreEqual(("bar", ""), new Regex("(?-i)bar", RegexOptions.IgnoreCase).GetSourceAndFlags());
Assert.AreEqual(("bar", "i"), new Regex("(?i-m)bar", RegexOptions.Multiline).GetSourceAndFlags());

// Only the leading group is stripped; later groups stay in the source.
Assert.AreEqual(("foo(?m)bar", "i"), new Regex("(?i)foo(?m)bar").GetSourceAndFlags());

// Non-modifier groups (e.g., non-capturing) are not touched.
Assert.AreEqual(("(?:foo)", ""), new Regex("(?:foo)").GetSourceAndFlags());

// Unsupported inline flags throw.
Assert.Throws<System.ArgumentException>(() => new Regex("(?n)foo").GetSourceAndFlags());
Assert.Throws<System.ArgumentException>(() => new Regex("(?x)foo").GetSourceAndFlags());
}
}
12 changes: 12 additions & 0 deletions src/Playwright.Tests/PageRouteTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,18 @@ public async Task ShouldBeAbortable()
Assert.AreEqual(1, failedRequests);
}

[PlaywrightTest("page-route.spec.ts", "should honour leading inline regex flags")]
public async Task ShouldHonourLeadingInlineRegexFlags()
{
await Page.RouteAsync(new Regex(@"(?i).+\.CSS$"), (route) => route.AbortAsync());

int failedRequests = 0;
Page.RequestFailed += (_, _) => ++failedRequests;
var response = await Page.GotoAsync(Server.Prefix + "/one-style.html");
Assert.True(response.Ok);
Assert.AreEqual(1, failedRequests);
}

[PlaywrightTest("page-route.spec.ts", "should be abortable with custom error codes")]
public async Task ShouldBeAbortableWithCustomErrorCodes()
{
Expand Down
5 changes: 3 additions & 2 deletions src/Playwright/Core/AssertionsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,9 @@ protected ExpectedTextValue ExpectedRegex(Regex pattern, ExpectedTextValue? opti
}

ExpectedTextValue textValue = options ?? new() { };
textValue.RegexSource = pattern.ToString();
textValue.RegexFlags = pattern.Options.GetInlineFlags();
var (source, flags) = pattern.GetSourceAndFlags();
textValue.RegexSource = source;
textValue.RegexFlags = flags;
return textValue;
}

Expand Down
20 changes: 12 additions & 8 deletions src/Playwright/Core/BrowserContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -332,17 +332,20 @@ public async Task<IAsyncDisposable> AddInitScriptAsync(string? script = null, st
[MethodImpl(MethodImplOptions.NoInlining)]
public async Task ClearCookiesAsync(BrowserContextClearCookiesOptions? options = default)
{
var nameRegex = options?.NameRegex?.GetSourceAndFlags();
var domainRegex = options?.DomainRegex?.GetSourceAndFlags();
var pathRegex = options?.PathRegex?.GetSourceAndFlags();
var @params = new Dictionary<string, object?>
{
["name"] = options?.Name ?? options?.NameString,
["nameRegexSource"] = options?.NameRegex?.ToString(),
["nameRegexFlags"] = options?.NameRegex?.Options.GetInlineFlags(),
["nameRegexSource"] = nameRegex?.Source,
["nameRegexFlags"] = nameRegex?.Flags,
["domain"] = options?.Domain ?? options?.DomainString,
["domainRegexSource"] = options?.DomainRegex?.ToString(),
["domainRegexFlags"] = options?.DomainRegex?.Options.GetInlineFlags(),
["domainRegexSource"] = domainRegex?.Source,
["domainRegexFlags"] = domainRegex?.Flags,
["path"] = options?.Path ?? options?.PathString,
["pathRegexSource"] = options?.PathRegex?.ToString(),
["pathRegexFlags"] = options?.PathRegex?.Options.GetInlineFlags(),
["pathRegexSource"] = pathRegex?.Source,
["pathRegexFlags"] = pathRegex?.Flags,
};

await SendMessageToServerAsync("clearCookies", @params).ConfigureAwait(false);
Expand Down Expand Up @@ -913,8 +916,9 @@ internal async Task RecordIntoHarAsync(string har, Page? page, BrowserContextRou
}
if (options?.UrlRegex != null)
{
recordHarArgs["urlRegexSource"] = options?.UrlRegex.ToString();
recordHarArgs["urlRegexFlags"] = options?.UrlRegex.Options.GetInlineFlags();
var (source, flags) = options.UrlRegex.GetSourceAndFlags();
recordHarArgs["urlRegexSource"] = source;
recordHarArgs["urlRegexFlags"] = flags;
}
recordHarArgs["mode"] = options?.UpdateMode ?? HarMode.Minimal;

Expand Down
3 changes: 2 additions & 1 deletion src/Playwright/Core/Locator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -616,8 +616,9 @@ private static string EscapeForAttributeSelector(Regex value, bool exact)

private static string EscapeRegexForSelector(Regex text)
{
var (source, flags) = text.GetSourceAndFlags();
// Even number of backslashes followed by the quote -> insert a backslash.
return Regex.Replace($"/{text}/{text.Options.GetInlineFlags()}", @"(^|[^\\])(\\\\)*([\""'`])", "$1$2\\$3").Replace(">>", "\\>\\>");
return Regex.Replace($"/{source}/{flags}", @"(^|[^\\])(\\\\)*([\""'`])", "$1$2\\$3").Replace(">>", "\\>\\>");
}

private static string EscapeForTextSelector(Regex text, bool? exact)
Expand Down
5 changes: 3 additions & 2 deletions src/Playwright/Core/RouteHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ public static List<Dictionary<string, object>> PrepareInterceptionPatterns(List<
}
else if (handler.urlMatcher.re != null)
{
pattern["regexSource"] = handler.urlMatcher.re.ToString();
pattern["regexFlags"] = handler.urlMatcher.re.Options.GetInlineFlags();
var (source, flags) = handler.urlMatcher.re.GetSourceAndFlags();
pattern["regexSource"] = source;
pattern["regexFlags"] = flags;
}

if (handler.urlMatcher.func != null)
Expand Down
5 changes: 3 additions & 2 deletions src/Playwright/Core/WebSocketRouteHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ public static List<Dictionary<string, object>> PrepareInterceptionPatterns(List<
}
else if (handler.urlMatcher.re != null)
{
pattern["regexSource"] = handler.urlMatcher.re.ToString();
pattern["regexFlags"] = handler.urlMatcher.re.Options.GetInlineFlags();
var (source, flags) = handler.urlMatcher.re.GetSourceAndFlags();
pattern["regexSource"] = source;
pattern["regexFlags"] = flags;
}
else
{
Expand Down
45 changes: 45 additions & 0 deletions src/Playwright/Helpers/RegexOptionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,51 @@ namespace Microsoft.Playwright.Helpers;
/// </summary>
internal static class RegexOptionsExtensions
{
private static readonly Regex LeadingInlineFlags = new(@"^\(\?([imnsx]*)(?:-([imnsx]+))?\)", RegexOptions.Compiled);

/// <summary>
/// Returns the regex pattern (with any leading inline flag group like <c>(?i)</c> stripped) and the
/// combined flag string, merging <see cref="Regex.Options"/> with the stripped inline flags.
/// </summary>
public static (string Source, string Flags) GetSourceAndFlags(this Regex regex)
{
var source = regex.ToString();
var options = regex.Options;
var match = LeadingInlineFlags.Match(source);
if (match.Success && (match.Groups[1].Length > 0 || match.Groups[2].Success))
{
ApplyInlineFlags(ref options, match.Groups[1].Value, set: true);
if (match.Groups[2].Success)
{
ApplyInlineFlags(ref options, match.Groups[2].Value, set: false);
}
source = source.Substring(match.Length);
}
return (source, options.GetInlineFlags());
}

private static void ApplyInlineFlags(ref RegexOptions options, string flags, bool set)
{
foreach (var c in flags)
{
var bit = c switch
{
'i' => RegexOptions.IgnoreCase,
's' => RegexOptions.Singleline,
'm' => RegexOptions.Multiline,
_ => throw new ArgumentException("Unsupported RegularExpression flags"),
};
if (set)
{
options |= bit;
}
else
{
options &= ~bit;
}
}
}

public static string GetInlineFlags(this System.Text.RegularExpressions.RegexOptions options)
{
string flags = string.Empty;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ internal static object Serialize(object? value, List<EvaluateArgumentGuidElement

if (value is Regex regex)
{
return new { r = new { p = regex.ToString(), f = regex.Options.GetInlineFlags() } };
var (p, f) = regex.GetSourceAndFlags();
return new { r = new { p, f } };
}

if (value is Guid guid)
Expand Down
Loading