Merge pull request #617 from Qtoss-AI/master
Add EnableResponseCallback
This commit is contained in:
commit
99820ef242
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -293,4 +293,5 @@ logs
|
|||
wwwroot
|
||||
appsettings.Production.json
|
||||
*.csproj.user
|
||||
env/
|
||||
env/
|
||||
FodyWeavers.*
|
||||
|
|
@ -36,6 +36,7 @@
|
|||
<PackageReference Include="System.Text.Json" Version="8.0.4" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="Rougamo.Fody" Version="4.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@ namespace BotSharp.Abstraction.Browsing;
|
|||
|
||||
public interface IWebPageResponseHook
|
||||
{
|
||||
void OnDataFetched(MessageInfo message, string url, string postData, string responsData);
|
||||
T? GetResponse<T>(MessageInfo message, string url, string? queryParameter = null);
|
||||
void OnDataFetched(MessageInfo message, WebPageResponseData response);
|
||||
T? GetResponse<T>(MessageInfo message, WebPageResponseFilter filter);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ public class ElementActionArgs
|
|||
|
||||
public string? PressKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Locator option
|
||||
/// </summary>
|
||||
public bool FirstIfMultipleFound { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Wait time in seconds
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ public class ElementLocatingArgs
|
|||
public bool Parent { get; set; }
|
||||
|
||||
public bool FailIfMultiple { get; set; }
|
||||
public bool IgnoreIfNotFound { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Draw outline around the element
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using BotSharp.Abstraction.Infrastructures;
|
||||
|
||||
namespace BotSharp.Abstraction.Browsing.Models;
|
||||
|
||||
public class MessageInfo
|
||||
public class MessageInfo : ICacheKey
|
||||
{
|
||||
public string AgentId { get; set; } = null!;
|
||||
public string UserId { get; set; } = null!;
|
||||
|
|
@ -8,4 +10,7 @@ public class MessageInfo
|
|||
public string? MessageId { get; set; }
|
||||
public string? TaskId { get; set; }
|
||||
public string StepId { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
public string GetCacheKey()
|
||||
=> $"{nameof(MessageInfo)}-{ContextId}";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,25 @@ public class PageActionArgs
|
|||
/// This value has to be set to true if you want to get the page XHR/ Fetch responses
|
||||
/// </summary>
|
||||
public bool OpenNewTab { get; set; } = false;
|
||||
|
||||
public bool EnableResponseCallback { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Exclude urls for XHR/ Fetch responses
|
||||
/// </summary>
|
||||
public string[]? ExcludeResponseUrls { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Only include urls for XHR/ Fetch responses
|
||||
/// </summary>
|
||||
public string[]? IncludeResponseUrls { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set to true, the response will be stored in memory
|
||||
/// </summary>
|
||||
public bool ResponseInMemory { get; set; } = false;
|
||||
public List<WebPageResponseData>? ResponseContainer { get; set; }
|
||||
|
||||
public bool UseExistingPage { get; set; } = false;
|
||||
|
||||
public bool WaitForNetworkIdle { get; set; } = true;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
namespace BotSharp.Abstraction.Browsing.Models;
|
||||
|
||||
public class WebPageResponseData
|
||||
{
|
||||
public string Url { get; set; } = null!;
|
||||
public string PostData { get; set; } = null!;
|
||||
public string ResponseData { get; set; } = null!;
|
||||
public bool ResponseInMemory { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
namespace BotSharp.Abstraction.Browsing.Models;
|
||||
|
||||
public class WebPageResponseFilter
|
||||
{
|
||||
public string Url { get; set; } = null!;
|
||||
public string[]? QueryParameters { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
namespace BotSharp.Abstraction.Infrastructures;
|
||||
|
||||
public interface ICacheKey
|
||||
{
|
||||
string GetCacheKey();
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
namespace BotSharp.Abstraction.Infrastructures;
|
||||
|
||||
public interface ICacheService
|
||||
{
|
||||
Task<T?> GetAsync<T>(string key);
|
||||
Task<object> GetAsync(string key, Type type);
|
||||
Task SetAsync<T>(string key, T value, TimeSpan? expiry);
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
using BotSharp.Abstraction.Infrastructures;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Rougamo;
|
||||
using Rougamo.Context;
|
||||
|
||||
namespace BotSharp.Core.Infrastructures;
|
||||
|
||||
public class SharpCacheAttribute : MoAttribute
|
||||
{
|
||||
public static IServiceProvider Services { get; set; } = null!;
|
||||
|
||||
private int _minutes;
|
||||
|
||||
public SharpCacheAttribute(int minutes = 60)
|
||||
{
|
||||
_minutes = minutes;
|
||||
}
|
||||
|
||||
public override void OnEntry(MethodContext context)
|
||||
{
|
||||
var settings = Services.GetRequiredService<SharpCacheSettings>();
|
||||
if (!settings.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cache = Services.GetRequiredService<ICacheService>();
|
||||
|
||||
var key = GetCacheKey(settings, context);
|
||||
var value = cache.GetAsync(key, context.TaskReturnType).Result;
|
||||
if (value != null)
|
||||
{
|
||||
context.ReplaceReturnValue(this, value);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnSuccess(MethodContext context)
|
||||
{
|
||||
var settings = Services.GetRequiredService<SharpCacheSettings>();
|
||||
if (!settings.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var httpContext = Services.GetRequiredService<IHttpContextAccessor>();
|
||||
if (httpContext.HttpContext.Response.Headers["Cache-Control"].ToString().Contains("no-store"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cache = Services.GetRequiredService<ICacheService>();
|
||||
|
||||
if (context.ReturnValue != null)
|
||||
{
|
||||
var key = GetCacheKey(settings, context);
|
||||
cache.SetAsync(key, context.ReturnValue, new TimeSpan(0, _minutes, 0)).Wait();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetCacheKey(SharpCacheSettings settings, MethodContext context)
|
||||
{
|
||||
var key = settings.Prefix + "-" + context.Method.Name;
|
||||
foreach (var arg in context.Arguments)
|
||||
{
|
||||
if (arg is null)
|
||||
{
|
||||
key += "-" + "<NULL>";
|
||||
}
|
||||
else if (arg is ICacheKey withCacheKey)
|
||||
{
|
||||
key += "-" + withCacheKey.GetCacheKey();
|
||||
}
|
||||
else
|
||||
{
|
||||
key += "-" + arg.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
namespace BotSharp.Abstraction.Infrastructures;
|
||||
|
||||
public class SharpCacheSettings
|
||||
{
|
||||
public bool Enabled { get; set; } = false;
|
||||
public string Prefix { get; set; } = "cache";
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
using BotSharp.Abstraction.Infrastructures;
|
||||
|
||||
namespace BotSharp.Abstraction.Utilities;
|
||||
|
||||
public class Pagination
|
||||
public class Pagination : ICacheKey
|
||||
{
|
||||
private int _page;
|
||||
private int _size;
|
||||
|
|
@ -39,6 +41,9 @@ public class Pagination
|
|||
}
|
||||
|
||||
public bool ReturnTotal { get; set; } = true;
|
||||
|
||||
public string GetCacheKey()
|
||||
=> $"{nameof(Pagination)}_{_page}_{_size}_{Sort}_{Order}";
|
||||
}
|
||||
|
||||
public class PagedItems<T>
|
||||
|
|
|
|||
|
|
@ -46,10 +46,13 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="packages\**" />
|
||||
<Compile Remove="Planning\**" />
|
||||
<Compile Remove="Translation\Models\**" />
|
||||
<EmbeddedResource Remove="packages\**" />
|
||||
<EmbeddedResource Remove="Planning\**" />
|
||||
<EmbeddedResource Remove="Translation\Models\**" />
|
||||
<None Remove="packages\**" />
|
||||
<None Remove="Planning\**" />
|
||||
<None Remove="Translation\Models\**" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
using BotSharp.Abstraction.Functions;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using BotSharp.Abstraction.Functions;
|
||||
using BotSharp.Core.Plugins;
|
||||
using BotSharp.Abstraction.Settings;
|
||||
using BotSharp.Abstraction.Options;
|
||||
using BotSharp.Abstraction.Messaging.JsonConverters;
|
||||
using BotSharp.Abstraction.Users.Settings;
|
||||
using BotSharp.Abstraction.Interpreters.Settings;
|
||||
using BotSharp.Abstraction.Infrastructures;
|
||||
|
||||
namespace BotSharp.Core;
|
||||
|
||||
|
|
@ -22,8 +23,15 @@ public static class BotSharpCoreExtensions
|
|||
|
||||
services.AddScoped<ISettingService, SettingService>();
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
|
||||
services.AddSingleton<DistributedLocker>();
|
||||
|
||||
// Register cache service
|
||||
var cacheSettings = new SharpCacheSettings();
|
||||
config.Bind("SharpCache", cacheSettings);
|
||||
services.AddSingleton(x => cacheSettings);
|
||||
services.AddSingleton<ICacheService, CacheService>();
|
||||
|
||||
RegisterPlugins(services, config);
|
||||
ConfigureBotSharpOptions(services, configOptions);
|
||||
|
||||
|
|
@ -61,6 +69,9 @@ public static class BotSharpCoreExtensions
|
|||
|
||||
app.ApplicationServices.GetRequiredService<PluginLoader>().Configure(app);
|
||||
|
||||
// Set root services for SharpCacheAttribute
|
||||
SharpCacheAttribute.Services = app.ApplicationServices;
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
using BotSharp.Abstraction.Infrastructures;
|
||||
using Newtonsoft.Json;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace BotSharp.Core.Infrastructures;
|
||||
|
||||
public class CacheService : ICacheService
|
||||
{
|
||||
private readonly BotSharpDatabaseSettings _settings;
|
||||
private static ConnectionMultiplexer redis = null!;
|
||||
|
||||
public CacheService(BotSharpDatabaseSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public async Task<T?> GetAsync<T>(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_settings.Redis))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (redis == null)
|
||||
{
|
||||
redis = ConnectionMultiplexer.Connect(_settings.Redis);
|
||||
}
|
||||
|
||||
var db = redis.GetDatabase();
|
||||
var value = await db.StringGetAsync(key);
|
||||
|
||||
if (value.HasValue)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(value);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public async Task<object> GetAsync(string key, Type type)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_settings.Redis))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (redis == null)
|
||||
{
|
||||
redis = ConnectionMultiplexer.Connect(_settings.Redis);
|
||||
}
|
||||
|
||||
var db = redis.GetDatabase();
|
||||
var value = await db.StringGetAsync(key);
|
||||
|
||||
if (value.HasValue)
|
||||
{
|
||||
return JsonConvert.DeserializeObject(value, type);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
|
||||
public async Task SetAsync<T>(string key, T value, TimeSpan? expiry)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_settings.Redis))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var db = redis.GetDatabase();
|
||||
await db.StringSetAsync(key, JsonConvert.SerializeObject(value), expiry);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,47 +6,54 @@ namespace BotSharp.Core.Infrastructures;
|
|||
public class DistributedLocker
|
||||
{
|
||||
private readonly BotSharpDatabaseSettings _settings;
|
||||
private static ConnectionMultiplexer connection;
|
||||
|
||||
public DistributedLocker(BotSharpDatabaseSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public async Task Lock(string resource, Func<Task> action, int timeoutInSeconds = 30)
|
||||
public async Task<T> Lock<T>(string resource, Func<Task<T>> action, int timeoutInSeconds = 30)
|
||||
{
|
||||
await ConnectToRedis();
|
||||
|
||||
var timeout = TimeSpan.FromSeconds(timeoutInSeconds);
|
||||
|
||||
var connection = await ConnectionMultiplexer.ConnectAsync(_settings.Redis);
|
||||
var @lock = new RedisDistributedLock(resource, connection.GetDatabase());
|
||||
await using (var handle = await @lock.TryAcquireAsync(timeout))
|
||||
{
|
||||
if (handle != null)
|
||||
{
|
||||
await action();
|
||||
}
|
||||
else
|
||||
if (handle == null)
|
||||
{
|
||||
Serilog.Log.Logger.Error($"Acquire lock for {resource} failed due to after {timeout}s timeout.");
|
||||
}
|
||||
|
||||
return await action();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Lock(string resource, Action action, int timeoutInSeconds = 30)
|
||||
public async Task<T> Lock<T>(string resource, Func<T> action, int timeoutInSeconds = 30)
|
||||
{
|
||||
await ConnectToRedis();
|
||||
|
||||
var timeout = TimeSpan.FromSeconds(timeoutInSeconds);
|
||||
|
||||
var connection = await ConnectionMultiplexer.ConnectAsync(_settings.Redis);
|
||||
var @lock = new RedisDistributedLock(resource, connection.GetDatabase());
|
||||
await using (var handle = await @lock.TryAcquireAsync(timeout))
|
||||
{
|
||||
if (handle != null)
|
||||
{
|
||||
action();
|
||||
}
|
||||
else
|
||||
if (handle == null)
|
||||
{
|
||||
Serilog.Log.Logger.Error($"Acquire lock for {resource} failed due to after {timeout}s timeout.");
|
||||
|
||||
}
|
||||
return action();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConnectToRedis()
|
||||
{
|
||||
if (connection == null)
|
||||
{
|
||||
connection = await ConnectionMultiplexer.ConnectAsync(_settings.Redis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ public class UserService : IUserService
|
|||
{
|
||||
new Claim(JwtRegisteredClaimNames.NameId, user.Id),
|
||||
new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName),
|
||||
new Claim(JwtRegisteredClaimNames.Email, user.Email),
|
||||
new Claim(JwtRegisteredClaimNames.Email, user?.Email ?? string.Empty),
|
||||
new Claim(JwtRegisteredClaimNames.GivenName, user?.FirstName ?? string.Empty),
|
||||
new Claim(JwtRegisteredClaimNames.FamilyName, user?.LastName ?? string.Empty),
|
||||
new Claim("source", user.Source),
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ public class PlaywrightInstance : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(pattern))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _contexts[contextId].Pages.LastOrDefault();
|
||||
}
|
||||
|
||||
|
|
@ -117,7 +122,12 @@ public class PlaywrightInstance : IDisposable
|
|||
return _contexts[ctxId];
|
||||
}
|
||||
|
||||
public async Task<IPage> NewPage(MessageInfo message, string[]? excludeResponseUrls = null)
|
||||
public async Task<IPage> NewPage(MessageInfo message,
|
||||
bool enableResponseCallback = false,
|
||||
bool responseInMemory = false,
|
||||
List<WebPageResponseData>? responseContainer = null,
|
||||
string[]? excludeResponseUrls = null,
|
||||
string[]? includeResponseUrls = null)
|
||||
{
|
||||
var context = await GetContext(message.ContextId);
|
||||
var page = await context.NewPageAsync();
|
||||
|
|
@ -127,13 +137,19 @@ public class PlaywrightInstance : IDisposable
|
|||
var js = @"Object.defineProperties(navigator, {webdriver:{get:()=>false}});";
|
||||
await page.AddInitScriptAsync(js);
|
||||
|
||||
if (!enableResponseCallback)
|
||||
{
|
||||
return page;
|
||||
}
|
||||
|
||||
page.Response += async (sender, e) =>
|
||||
{
|
||||
if (e.Status != 204 &&
|
||||
e.Headers.ContainsKey("content-type") &&
|
||||
e.Headers["content-type"].Contains("application/json") &&
|
||||
(e.Request.ResourceType == "fetch" || e.Request.ResourceType == "xhr") &&
|
||||
(excludeResponseUrls == null || !excludeResponseUrls.Any(url => e.Url.ToLower().Contains(url))))
|
||||
(excludeResponseUrls == null || !excludeResponseUrls.Any(url => e.Url.ToLower().Contains(url))) &&
|
||||
(includeResponseUrls == null || includeResponseUrls.Any(url => e.Url.ToLower().Contains(url))))
|
||||
{
|
||||
Serilog.Log.Information($"{e.Request.Method}: {e.Url}");
|
||||
JsonElement? json = null;
|
||||
|
|
@ -148,10 +164,23 @@ public class PlaywrightInstance : IDisposable
|
|||
Serilog.Log.Warning($"Response status: {e.Status} {e.StatusText}, OK: {e.Ok}");
|
||||
}
|
||||
|
||||
var result = new WebPageResponseData
|
||||
{
|
||||
Url = e.Url.ToLower(),
|
||||
PostData = e.Request?.PostData ?? string.Empty,
|
||||
ResponseData = JsonSerializer.Serialize(json),
|
||||
ResponseInMemory = responseInMemory
|
||||
};
|
||||
|
||||
if (responseContainer != null && responseInMemory)
|
||||
{
|
||||
responseContainer.Add(result);
|
||||
}
|
||||
|
||||
var webPageResponseHooks = _services.GetServices<IWebPageResponseHook>();
|
||||
foreach (var hook in webPageResponseHooks)
|
||||
{
|
||||
hook.OnDataFetched(message, e.Url.ToLower(), e.Request?.PostData ?? string.Empty, JsonSerializer.Serialize(json));
|
||||
hook.OnDataFetched(message, result);
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
|
|
@ -160,7 +189,7 @@ public class PlaywrightInstance : IDisposable
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex.ToString());
|
||||
Serilog.Log.Error($"{e.Url}\r\n" + ex.ToString());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,11 +13,24 @@ public partial class PlaywrightWebDriver
|
|||
|
||||
ILocator locator = page.Locator(result.Selector);
|
||||
var count = await locator.CountAsync();
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
Serilog.Log.Error($"Element not found: {result.Selector}");
|
||||
return;
|
||||
}
|
||||
else if (count > 1)
|
||||
{
|
||||
if(!action.FirstIfMultipleFound)
|
||||
{
|
||||
Serilog.Log.Error($"Multiple eElements were found: {result.Selector}");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
locator = page.Locator(result.Selector).First;// 匹配到多个时取第一个,否则当await locator.ClickAsync();匹配到多个就会抛异常。
|
||||
}
|
||||
}
|
||||
|
||||
if (action.Action == BroswerActionEnum.Click)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ public partial class PlaywrightWebDriver
|
|||
{
|
||||
var page = args.UseExistingPage ?
|
||||
_instance.GetPage(message.ContextId, pattern: args.Url) :
|
||||
await _instance.NewPage(message, excludeResponseUrls: args.ExcludeResponseUrls);
|
||||
await _instance.NewPage(message, enableResponseCallback: args.EnableResponseCallback,
|
||||
responseInMemory: args.ResponseInMemory,
|
||||
responseContainer: args.ResponseContainer,
|
||||
excludeResponseUrls: args.ExcludeResponseUrls,
|
||||
includeResponseUrls: args.IncludeResponseUrls);
|
||||
|
||||
if (args.UseExistingPage && page != null && page.Url == args.Url)
|
||||
{
|
||||
|
|
@ -23,7 +27,22 @@ public partial class PlaywrightWebDriver
|
|||
|
||||
if (args.UseExistingPage && args.OpenNewTab && page != null && page.Url == "about:blank")
|
||||
{
|
||||
page = await _instance.NewPage(message, excludeResponseUrls: args.ExcludeResponseUrls);
|
||||
page = await _instance.NewPage(message,
|
||||
enableResponseCallback: args.EnableResponseCallback,
|
||||
responseInMemory: args.ResponseInMemory,
|
||||
responseContainer: args.ResponseContainer,
|
||||
excludeResponseUrls: args.ExcludeResponseUrls,
|
||||
includeResponseUrls: args.IncludeResponseUrls);
|
||||
}
|
||||
|
||||
if (page == null)
|
||||
{
|
||||
page = await _instance.NewPage(message,
|
||||
enableResponseCallback: args.EnableResponseCallback,
|
||||
responseInMemory: args.ResponseInMemory,
|
||||
responseContainer: args.ResponseContainer,
|
||||
excludeResponseUrls: args.ExcludeResponseUrls,
|
||||
includeResponseUrls: args.IncludeResponseUrls);
|
||||
}
|
||||
|
||||
var response = await page.GotoAsync(args.Url, new PageGotoOptions
|
||||
|
|
|
|||
|
|
@ -68,8 +68,17 @@ public partial class PlaywrightWebDriver
|
|||
|
||||
if (count == 0)
|
||||
{
|
||||
result.Message = $"Can't locate element by keyword {location.Text}";
|
||||
_logger.LogError(result.Message);
|
||||
if (location.IgnoreIfNotFound)
|
||||
{
|
||||
result.Message = $"Can't locate element by keyword {location.Text} and Ignored";
|
||||
_logger.LogWarning(result.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Message = $"Can't locate element by keyword {location.Text}";
|
||||
_logger.LogError(result.Message);
|
||||
}
|
||||
|
||||
}
|
||||
else if (count == 1)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue