60 public class PuppeteerCommunicator
62 private string _puppeteerUrl;
63 private ClientWebSocket _webSocket;
65 public string webSocketUrl {
get;
private set; }
67 private int _activeWindowIndex = 0;
73 get => Browser.ServerResponseCode;
79 private Dictionary<string, long> FrameIdToContextId {
get;
set; } =
new Dictionary<string, long>(StringComparer.Ordinal);
83 private string CurrentFrameSessionId {
get;
set; }
88 private long? CurrentContextId {
get;
set; }
92 private string CurrentFrameId {
get;
set; }
100 private string CurrentRootObjectId {
get;
set; }
110 internal async Task EnablePageEvents()
112 await SendCommand<object>(DevToolsMethods.PageEnable,
new { },
GetEffectiveSessionId()).ConfigureAwait(
false);
115 private ConcurrentDictionary<long, TaskCompletionSource<dynamic>> _responseTasks {
get; } =
new ConcurrentDictionary<long, TaskCompletionSource<dynamic>>();
116 private ConcurrentQueue<(
string Event, dynamic Data)> _events {
get; } =
new ConcurrentQueue<(string, dynamic)>();
120 internal ConcurrentQueue<WindowSession> _windowSessionsQueue {
get;
set; } =
new ConcurrentQueue<WindowSession>();
128 var tabQueue = GetActiveWindowTabQueue();
129 return tabQueue ??
new ConcurrentQueue<KeyValuePair<string, string>>();
137 get => ((PuppeteerClient)PuppeteerClient)._currentTargetId;
139 private int _commandId = 0xdead;
143 internal bool _isRunning {
get;
set; }
147 internal bool _usePipes {
get;
set; }
148 private CancellationTokenSource _cts;
149 private readonly SemaphoreSlim _sendSemaphore =
new SemaphoreSlim(1, 1);
153 internal IPuppeteerClient PuppeteerClient {
get;
set; }
157 internal Browser Browser {
get;
set; }
161 private bool _suppressNetworkEvents {
get;
set; } =
false;
165 internal bool CapturingCalls {
get;
set; } =
false;
166 internal List<GPALCall> CapturedCalls {
get; } =
new List<GPALCall>();
176 return new List<GPALCall>(CapturedCalls);
184 CapturedCalls.Clear();
188 private readonly Dictionary<string, GPALCall> _capturedById =
new Dictionary<string, GPALCall>();
192 internal Task ReceiveTask {
get;
set; }
196 internal TaskCompletionSource<bool> _readerReadyTcs {
get;
set; }
200 List<string> lastErrorMessage =
new List<string>();
201 bool supressedMessage =
false;
202 string lastSessionToken;
207 internal class CastDevice
209 public CastDevice(
string name,
string id)
218 internal List<CastDevice> CastDevices =
new List<CastDevice>();
222 internal string SinkName {
get;
set; } =
null;
224 internal PuppeteerCommunicator(
string puppeteerUrl, IBrowser browser,
bool usePipes =
false,
bool capturingCalls =
false)
228 CapturingCalls = capturingCalls;
230 Browser = (Browser)browser;
234 PuppeteerClient =
new PuppeteerClient();
235 _usePipes = usePipes;
236 _windowSessionsQueue =
new ConcurrentQueue<WindowSession>();
238 _puppeteerUrl = puppeteerUrl;
242 var initTask = InitializeCommunication(Browser);
247 initFinished = initTask.Wait(TimeSpan.FromSeconds(45));
249 catch (AggregateException ex)
252 Exception reason = ex.Flatten().InnerExceptions.FirstOrDefault() ?? ex;
254 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(reason).Throw();
259 if (
false == initFinished)
261 string msg = $
"Puppeteer communication initialization timed out talking to [{puppeteerUrl}]. Workflow canot run without it, Try again.";
263 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg,
this, GPALObjectType.PuppeteerCommunicator);
267 throw new GPALException($
"{GPAL.MyMethodName()}: {msg}");
269 ReceiveTask = initTask.GetAwaiter().GetResult();
273 if (!
string.IsNullOrEmpty(puppeteerUrl))
274 PuppeteerClient.WithAPIBase(puppeteerUrl).WithPuppeteerCommunicator(
this);
276 Browser.Started =
true;
279 public PuppeteerCommunicator()
290 private const int MaxConnectAttempts = 3;
296 private const int ReaderStopWaitMs = 2_000;
303 private int _connectAttempts;
305 internal async Task Reconnect()
308 if (
false == _usePipes)
310 if (_webSocket !=
null)
314 _webSocket.Dispose();
319 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Cannot restart CDP pipes. Please restart workflow",
this, GPALObjectType.PuppeteerCommunicator);
326 if (
null != ReceiveTask)
327 await Task.WhenAny(ReceiveTask, Task.Delay(ReaderStopWaitMs)).ConfigureAwait(
false);
331 await InitializeCommunication(Browser).ConfigureAwait(
false);
345 private async Task<Task> InitializeCommunication(Browser Browser)
347 dynamic sessionId =
null;
348 dynamic session =
null;
349 dynamic targets =
null;
350 bool _foundWhatsNew =
false;
353 _readerReadyTcs =
new TaskCompletionSource<bool>();
354 _cts =
new CancellationTokenSource();
357 if (
true == _usePipes)
359 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
"Using Puppeteer Pipes",
this, GPALObjectType.PuppeteerCommunicator);
363 if (
true == Browser?.Process?.HasExited)
365 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Chrome process not running or exited for pipe transport",
this, GPALObjectType.PuppeteerCommunicator);
369 ourTask = Task.Run(async () =>
373 await ReceivePipeMessages(_cts.Token).ConfigureAwait(
false);
377 _readerReadyTcs.TrySetCanceled();
386 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Pipe initialization failed",
this, GPALObjectType.PuppeteerCommunicator, ex);
392 var ws =
new ClientWebSocket();
393 ws.Options.SetBuffer(65536, 65536);
394 ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
396 _webSocket.Options.KeepAliveInterval = TimeSpan.Zero;
397 webSocketUrl = GetWebSocketUrl(_puppeteerUrl);
399 if (
string.IsNullOrEmpty(webSocketUrl))
403 string msg = $
"The browser at [{_puppeteerUrl}] never opened its debug port, so there is nothing to drive";
405 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg,
this, GPALObjectType.PuppeteerCommunicator);
407 throw new GPALException($
"{GPAL.MyMethodName()}: {msg}");
410 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Using Puppeteer Ports Connecting to WebSocket: [{webSocketUrl}]",
this, GPALObjectType.PuppeteerCommunicator);
413 await _webSocket.ConnectAsync(
new Uri(webSocketUrl), _cts.Token).ConfigureAwait(
false);
414 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"WebSocket state after connect: [{_webSocket.State}]",
this, GPALObjectType.PuppeteerCommunicator);
416 _connectAttempts = 0;
419 await Task.Delay(10).ConfigureAwait(
false);
422 ourTask = Task.Run(async () => await ReceiveMessages(_cts.Token).ConfigureAwait(
false));
430 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"WebSocket connection failed on attempt [{_connectAttempts}] of [{MaxConnectAttempts}]",
this, GPALObjectType.PuppeteerCommunicator, ex);
432 if (MaxConnectAttempts <= _connectAttempts)
434 string msg = $
"The browser at [{_puppeteerUrl}] refused its debug socket on [{_connectAttempts}] attempts, so there is nothing to drive";
436 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg,
this, GPALObjectType.PuppeteerCommunicator);
438 throw new GPALException($
"{GPAL.MyMethodName()}: {msg}");
443 await Task.Delay(_connectAttempts * 1_000).ConfigureAwait(
false);
445 await Reconnect().ConfigureAwait(
false);
447 return await InitializeCommunication(Browser).ConfigureAwait(
false);
454 await SendCommand<object>(DevToolsMethods.TargetSetDiscoverTargets,
new { discover = true },
null).ConfigureAwait(
false);
457 for (
int cnt = 3; 0 < cnt &&
false == _foundWhatsNew; cnt--)
459 targets = await SendCommand<object>(DevToolsMethods.TargetGetTargets,
new { },
null).ConfigureAwait(
false);
460 if (targets ==
null || targets.targetInfos ==
null)
463 string msg = $
"The browser at [{_puppeteerUrl}] reported no targets, so there is no page to drive";
465 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg,
this, GPALObjectType.PuppeteerCommunicator);
467 throw new GPALException($
"{GPAL.MyMethodName()}: {msg}");
489 await SendCommand<object>(DevToolsMethods.TargetSetDiscoverTargets,
new { discover = true },
null).ConfigureAwait(
false);
493 var pageTarget = ((IEnumerable<dynamic>)targets.targetInfos)
494 .FirstOrDefault(t => t.type ==
"page" && t.url !=
"");
496 if (pageTarget !=
null)
498 var targetId = (string)pageTarget.targetId;
499 session = await SendCommand<object>(DevToolsMethods.TargetAttachToTarget,
new
503 },
null).ConfigureAwait(
false);
504 sessionId = (string)session?.sessionId;
506 if (sessionId !=
null)
508 string browserContextId = pageTarget.browserContextId?.ToString() ??
"";
509 var tabQueue =
new ConcurrentQueue<KeyValuePair<string, string>>();
510 tabQueue.Enqueue(
new KeyValuePair<string, string>(targetId, sessionId));
511 _windowSessionsQueue.Enqueue(
new WindowSession(browserContextId, tabQueue, 0));
512 _activeWindowIndex = 0;
513 ((PuppeteerClient)PuppeteerClient)._currentTargetId = targetId;
514 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Attached to targetId: [{targetId}], sessionId: [{sessionId}], URL: [{pageTarget.url}]",
this, GPALObjectType.PuppeteerCommunicator);
519 if (
true == CapturingCalls)
520 await
CaptureCalls(
true, sessionId).ConfigureAwait(
false);
524 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to attach to targetId: [{targetId}]",
this, GPALObjectType.PuppeteerCommunicator);
529 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"No page target found after initialization",
this, GPALObjectType.PuppeteerCommunicator);
534 catch (ObjectDisposedException ex) when (ex.ObjectName ==
"System.Net.WebSockets.ClientWebSocket")
536 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"WebSocket disposed",
this, GPALObjectType.Puppeteer, ex);
537 await Reconnect().ConfigureAwait(
false);
538 return await InitializeCommunication(Browser).ConfigureAwait(
false);
542 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Initialization failed",
this, GPALObjectType.Puppeteer, ex);
547 ReceiveTask = ourTask;
557 private async Task InitializeWebSocket()
559 webSocketUrl ??= GetWebSocketUrl(_puppeteerUrl);
560 if (
string.IsNullOrEmpty(webSocketUrl))
562 string msg = $
"The browser at [{_puppeteerUrl}] never opened its debug port, so there is nothing to reconnect to";
564 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg,
this, GPALObjectType.PuppeteerCommunicator);
566 throw new GPALException($
"{GPAL.MyMethodName()}: {msg}");
569 _webSocket =
new ClientWebSocket();
570 await _webSocket.ConnectAsync(
new Uri(webSocketUrl), _cts.Token).ConfigureAwait(
false);
580 private async Task ApplyStealthProtection(
string sessionId =
null)
584 string antiAntiBotScript =
@"
586 var originalError = Error;
587 Object.defineProperty(Error.prototype, 'stack', {
593 throw new originalError();
600 window.Error = new Proxy(originalError, {
601 construct(target, args) {
602 var instance = new target(...args);
603 return Object.freeze(instance);
607 // Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
609 // Enhanced toString override to hide script contents
611 // Store the original toString
612 const originalToString = Object.prototype.toString;
614 // Create a proxy handler to intercept toString calls
616 apply: function(target, thisArg, argumentsList) {
617 if (thisArg instanceof Function && /antiAntiBotScript|CDP/.test(originalToString.call(thisArg))) {
618 return 'function () { [native code] }';
620 return target.apply(thisArg, argumentsList);
624 // Wrap toString with a proxy
625 Object.prototype.toString = new Proxy(originalToString, handler);
627 console.log('toString override applied via Proxy');
630 console.log('CDP protection with toString override applied');
637 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Stealth CDP protection with toString override applied via Fetch.requestPaused.",
this, GPALObjectType.Browser);
639 [DllImport(
"user32.dll")]
640 private static extern bool ShowWindow(IntPtr hWnd,
int nCmdShow);
648 internal const int DefaultCommandTimeoutSeconds = 10;
649 internal const int LongCommandTimeoutSeconds = 60;
653 private static readonly HashSet<DevToolsMethods> slowCommands =
new HashSet<DevToolsMethods>
655 DevToolsMethods.PageNavigate,
656 DevToolsMethods.PageNavigateToHistoryEntry,
657 DevToolsMethods.PageReload,
658 DevToolsMethods.PageCaptureScreenshot,
661 public async Task<dynamic> SendCommand<TParameters>(DevToolsMethods method, TParameters parameters,
string sessionId,
int retries = 3,
bool doNotGetSemaphore =
false,
int timeoutSeconds = 0)
663 const int SW_MINIMIZE = 6;
666 if (0 >= timeoutSeconds)
667 timeoutSeconds = slowCommands.Contains(method) ? LongCommandTimeoutSeconds : DefaultCommandTimeoutSeconds;
668 const int SW_RESTORE = 9;
669 const int SW_MAXIMIZE = 3;
675 if (method.ToString().StartsWith(
"Target") &&
null != sessionId)
677 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Session ID [{sessionId}] cannot be used for 'Target' CDP commands. Using null.",
this, GPALObjectType.PuppeteerCommunicator);
682 if (method == DevToolsMethods.MinimizeWindow)
686 if (
false == Browser?.Process?.HasExited)
688 ShowWindow(Browser.Process.MainWindowHandle, SW_MINIMIZE);
691 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"No valid browser process to minimize",
this, GPALObjectType.PuppeteerCommunicator);
696 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Failed to minimize window",
this, GPALObjectType.PuppeteerCommunicator, ex);
700 else if (method == DevToolsMethods.RestoreWindow)
704 if (
false == Browser?.Process?.HasExited)
706 ShowWindow(Browser.Process.MainWindowHandle, SW_RESTORE);
709 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"No valid browser process to restore",
this, GPALObjectType.PuppeteerCommunicator);
714 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Failed to restore window",
this, GPALObjectType.PuppeteerCommunicator, ex);
718 else if (method == DevToolsMethods.MaximizeWindow)
722 if (
false == Browser?.Process?.HasExited)
724 ShowWindow(Browser.Process.MainWindowHandle, SW_MAXIMIZE);
727 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"No valid browser process to maximize",
this, GPALObjectType.PuppeteerCommunicator);
732 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Failed to maximize window",
this, GPALObjectType.PuppeteerCommunicator, ex);
736 else if (method == DevToolsMethods.CheckNetworkIdle)
741 if (
true == Browser?.Process?.HasExited)
743 string msg =
"The browser process has exited, so there is nothing left to send a command to";
745 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg,
this, GPALObjectType.PuppeteerCommunicator);
750 BrowserHelper.KillAllRunningProcesses(
true, Browser);
754 throw new GPALException($
"{GPAL.MyMethodName()}: {msg}");
756 else if (
false == _usePipes && (_webSocket ==
null || _webSocket.State != WebSocketState.Open))
758 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"WebSocket in [{_webSocket?.State}] state, reconnecting",
this, GPALObjectType.PuppeteerCommunicator);
759 await Reconnect().ConfigureAwait(
false);
766 if (
false == doNotGetSemaphore)
767 await _sendSemaphore.WaitAsync(_cts.Token).ConfigureAwait(
false);
771 long id = Interlocked.Increment(ref _commandId);
777 var tcs =
new TaskCompletionSource<dynamic>(TaskCreationOptions.RunContinuationsAsynchronously);
778 _responseTasks[id] = tcs;
780 var command = DevToolsCommandBuilder.BuildCommand(method, parameters,
id, sessionId, rootNodeId);
781 var bytes = Encoding.UTF8.GetBytes(command);
785 var stream = Browser.BrowserSettings.InboundPipe;
788 var lengthBytes = BitConverter.GetBytes((uint)bytes.Length);
789 if (BitConverter.IsLittleEndian)
790 Array.Reverse(lengthBytes);
791 await stream.WriteAsync(lengthBytes, 0, lengthBytes.Length, _cts.Token).ConfigureAwait(
false);
794 await stream.WriteAsync(bytes, 0, bytes.Length, _cts.Token).ConfigureAwait(
false);
797 await stream.FlushAsync(_cts.Token).ConfigureAwait(
false);
801 await _webSocket.SendAsync(
new ArraySegment<byte>(bytes), WebSocketMessageType.Text,
true, _cts.Token).ConfigureAwait(
false);
805 dynamic message =
null;
808 message = await tcs.Task.TimeoutAfter(TimeSpan.FromSeconds(timeoutSeconds), method.ToString()).ConfigureAwait(
false);
810 catch (TimeoutException)
812 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Command [{method}] timed out, releasing semaphore.",
this, GPALObjectType.PuppeteerCommunicator);
814 _responseTasks.TryRemove(
id, out _);
820 _responseTasks.TryRemove(
id, out _);
825 if (message is
string)
827 if (
true ==
"Session with given id not found.".Equals(message))
831 ClearCurrentSessionId();
832 CurrentFrameSessionId =
null;
837 else if (
true ==
"Sink not found".Equals(message))
839 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"DIAL devices not supported. Use ID from chrome://media-router-internals/",
this, GPALObjectType.PuppeteerCommunicator);
842 string msg = $
"CDP error : [{message}].";
843 if (
false == lastErrorMessage.Contains(msg))
845 lastErrorMessage.Add(msg);
846 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg,
this, GPALObjectType.PuppeteerCommunicator);
847 supressedMessage =
false;
849 else if (
false == supressedMessage)
851 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", msg, GPALObjectType.Other);
852 supressedMessage =
true;
855 throw new ApplicationException(msg);
857 else if (message?.error !=
null)
859 if (
true ==
"Session with given id not found.".Equals(message?.error?.message?.ToString()))
863 ClearCurrentSessionId();
864 CurrentFrameSessionId =
null;
870 string msg = $
"CDP error : [{message.error.message}].";
871 if (
false == lastErrorMessage.Contains(msg))
873 lastErrorMessage.Add(msg);
874 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg,
this, GPALObjectType.PuppeteerCommunicator);
875 supressedMessage =
false;
877 else if (
false == supressedMessage)
879 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", msg, GPALObjectType.Other);
880 supressedMessage =
true;
883 throw new ApplicationException(msg);
891 if (
null == resultExtractors)
893 var protocolJson = GetProtocolJSON();
894 resultExtractors = CdpResultExtractors.Build(protocolJson);
897 if (resultExtractors !=
null && resultExtractors.TryGetValue(method, out var extractor))
899 if (
null == message || message is
string || message.result ==
null)
901 string msg = $
"Command [{method}] returned null result.";
902 if (
false == lastErrorMessage.Contains(msg))
904 lastErrorMessage.Add(msg);
905 GPAL.PublishSimpleEvent(GPALEventType.WARNING, msg,
this, GPALObjectType.PuppeteerCommunicator);
906 supressedMessage =
false;
908 else if (
false == supressedMessage)
910 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", msg, GPALObjectType.Other);
911 supressedMessage =
true;
915 data = extractor((Newtonsoft.Json.Linq.JObject)message?.result);
919 data = message.result?.ToObject<dynamic>() ??
new { };
932 if (
false == doNotGetSemaphore)
933 _sendSemaphore.Release();
938 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Command [{method}][{parameters}] failed.",
null, GPALObjectType.None, ex);
954 string identifier =
null;
957 if (
true == Browser.UseSelenium)
960 identifier =
BrowserHelper.SeleniumAddScriptToEvaluateOnNewDocument(Browser.BrowserSettings, scriptToInject);
962 else if (
true == Browser.UsePuppeteer)
965 var result = await SendCommand<object>(DevToolsMethods.PageAddScriptToEvaluateOnNewDocument,
new
967 source = scriptToInject
968 }, sessionId ?? ((PuppeteerClient)PuppeteerClient)._currentTargetId).ConfigureAwait(
false);
969 identifier = result?.identifier?.ToString();
972 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Script injected via CDP for sessionId: [{sessionId ?? ((PuppeteerClient)PuppeteerClient)._currentTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
976 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to inject script via CDP",
this, GPALObjectType.Puppeteer, ex);
991 if (
string.IsNullOrEmpty(identifier))
999 if (
true == Browser.UseSelenium)
1003 else if (
true == Browser.UsePuppeteer)
1005 await SendCommand<object>(DevToolsMethods.PageRemoveScriptToEvaluateOnNewDocument,
new
1008 }, sessionId ?? ((PuppeteerClient)PuppeteerClient)._currentTargetId).ConfigureAwait(
false);
1011 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Injected script [{identifier}] removed via CDP for sessionId: [{sessionId ?? ((PuppeteerClient)PuppeteerClient)._currentTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
1013 catch (Exception ex)
1015 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to remove injected script via CDP",
this, GPALObjectType.Puppeteer, ex);
1020 private readonly List<string> _injectedScriptIdentifiers =
new List<string>();
1026 public async Task<string>
InjectScript(
string scriptToInject,
string sessionId =
null)
1029 if (!
string.IsNullOrEmpty(identifier))
1031 _injectedScriptIdentifiers.Add(identifier);
1041 foreach (var identifier
in _injectedScriptIdentifiers)
1045 _injectedScriptIdentifiers.Clear();
1203 internal async Task ReceivePipeMessages(CancellationToken ct)
1206 _readerReadyTcs?.SetResult(
true);
1209 if (stream ==
null || !stream.IsConnected || !stream.CanRead)
1211 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"OutboundPipe null, disconnected, or not readable",
this, GPALObjectType.PuppeteerCommunicator);
1215 var encoding = Encoding.UTF8;
1218 void ProcessMessageString(
string text)
1222 dynamic message = Newtonsoft.Json.JsonConvert.DeserializeObject(text);
1223 if (message ==
null)
return;
1225 if (message.id !=
null)
1227 if (_responseTasks.TryRemove((
int)message.id, out TaskCompletionSource<dynamic> tcs))
1229 if (message.error !=
null)
1231 string errorMessage = (message.error.message !=
null ? message.error.message.ToString() :
1232 message.error.ToString()) ??
"Unknown CDP error";
1233 if (!errorMessage.Contains(
"wasn't found"))
1235 tcs.TrySetResult(errorMessage);
1239 tcs.TrySetResult(message);
1243 else if (message.method !=
null)
1245 string method = (string)message.method;
1249 if (
true == method.StartsWith(
"Page.") &&
"Page.javascriptDialogOpening" != method)
1252 dynamic paramsData = message.@params;
1253 string title = paramsData?.targetInfo?.title;
1254 string targetType = paramsData?.targetInfo?.type?.ToString();
1255 string sessionId = message.sessionId?.ToString() ?? ((PuppeteerClient)PuppeteerClient)._currentTargetId;
1256 string url = paramsData?.targetInfo?.url?.ToString() ??
"unknown";
1258 if (method.StartsWith(
"Network."))
1260 if (method ==
"Network.requestWillBeSent" ||
1261 method ==
"Network.loadingFinished" ||
1262 method ==
"Network.loadingFailed" ||
1263 method ==
"Network.requestServedFromCache" ||
1264 method ==
"Network.requestWillBeSentExtraInfo")
1266 _events.Enqueue((method, paramsData));
1271 if (
true == CapturingCalls &&
"Network.requestWillBeSent" == method)
1272 RecordCall(paramsData);
1273 else if (
true == CapturingCalls &&
"Network.responseReceived" == method)
1274 RecordStatus(paramsData);
1276 if (_suppressNetworkEvents)
1283 if (
"Page.javascriptDialogOpening" == method &&
null != Browser.BrowserSettings.DialogsAccepted)
1285 bool accept =
true == Browser.BrowserSettings.DialogsAccepted;
1286 string typed = Browser.BrowserSettings.DialogText;
1288 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Answering a [{paramsData?.type}] with accept [{accept}] text [{typed}]",
this, GPALObjectType.PuppeteerCommunicator);
1293 _ = SendCommand<object>(DevToolsMethods.PageHandleJavaScriptDialog,
1294 null == typed ? (
object)
new { accept } :
new { accept, promptText = typed },
1295 sessionId, 3,
true);
1298 if (method ==
"Target.targetCreated" && _sendSemaphore.CurrentCount == 1)
1300 if (
true == targetType?.Equals(
"page") &&
false == url?.StartsWith(
"chrome://") &&
false == url?.Equals(
"about:blank"))
1302 string targetId = paramsData?.targetInfo?.targetId?.ToString();
1303 string browserContextId = paramsData?.targetInfo?.browserContextId?.ToString();
1304 if (!
string.IsNullOrEmpty(targetId))
1308 _ = HandleNewTarget(targetId, browserContextId,
null,
true);
1309 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Detected new uncontrolled tab: TargetId [{targetId}] in Window [{browserContextId}]",
this, GPALObjectType.PuppeteerCommunicator);
1314 GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $
"Received (pipe): [{text}]",
this, GPALObjectType.PuppeteerCommunicator);
1317 catch (Exception ex)
1319 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Error processing pipe message",
this, GPALObjectType.PuppeteerCommunicator, ex);
1325 var pending =
new List<byte>();
1326 var chunk =
new byte[65536];
1330 while (_isRunning && !Browser.BrowserSettings.Process.HasExited && !ct.IsCancellationRequested)
1332 var readTask = stream.ReadAsync(chunk, 0, chunk.Length, ct);
1334 if (await Task.WhenAny(readTask, Task.Delay(5000, ct)).ConfigureAwait(
false) != readTask)
1336 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"No data on OutboundPipe after 5s - Chrome not responding",
this, GPALObjectType.PuppeteerCommunicator);
1340 int bytesRead = readTask.Result;
1345 for (
int i = 0; i < bytesRead; i++)
1349 pending.Add(chunk[i]);
1354 if (0 < pending.Count)
1356 ProcessMessageString(encoding.GetString(pending.ToArray()));
1362 catch (Exception ex)
1364 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"ReceivePipeMessages failed",
this, GPALObjectType.PuppeteerCommunicator, ex);
1378 private async Task ReceiveMessages(CancellationToken ct)
1380 var buffer =
new byte[102400];
1382 while (_isRunning && _webSocket.State == WebSocketState.Open)
1386 if (ct.IsCancellationRequested)
1391 await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure,
"Cancellation requested", CancellationToken.None).ConfigureAwait(
false);
1392 _webSocket.Dispose();
1394 catch (Exception ex)
1396 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Error during WebSocket cleanup on cancellation",
this, GPALObjectType.PuppeteerCommunicator, ex);
1403 using var ms =
new System.IO.MemoryStream();
1404 WebSocketReceiveResult result;
1407 result = await _webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), ct).ConfigureAwait(
false);
1408 ms.Write(buffer, 0, result.Count);
1409 }
while (!result.EndOfMessage);
1413 if (result.MessageType == WebSocketMessageType.Close)
1415 await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure,
"WebSocket closed", CancellationToken.None).ConfigureAwait(
false);
1419 catch (OperationCanceledException)
1423 catch (WebSocketException ex)
1425 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"WebSocket error",
this, GPALObjectType.PuppeteerCommunicator, ex);
1429 if (_webSocket.State == WebSocketState.Closed || _webSocket.State == WebSocketState.Aborted)
1431 try { await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure,
"Cleanup", CancellationToken.None).ConfigureAwait(
false); }
catch { }
1432 _webSocket.Dispose();
1439 var json = Encoding.UTF8.GetString(ms.ToArray());
1440 var message = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);
1442 if (message?.
id !=
null)
1444 if (_responseTasks.TryRemove((
int)message.id, out TaskCompletionSource<dynamic> tcs))
1446 if (message.error !=
null)
1448 string errorMessage = (message.error.message !=
null ? message.error.message.ToString() :
1449 message.error.ToString()) ??
"Unknown CDP error";
1450 if (
false == errorMessage.Contains(
"wasn't found"))
1451 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, errorMessage,
this, GPALObjectType.PuppeteerCommunicator);
1452 tcs.SetResult(errorMessage);
1455 tcs.SetResult(message);
1458 else if (message.method !=
null)
1460 string method = (string)message.method;
1466 if (
true == method.StartsWith(
"Page.") &&
"Page.javascriptDialogOpening" != method)
1469 dynamic paramsData = message.@params;
1470 string title = paramsData.targetInfo?.title;
1471 string targetType = paramsData.targetInfo?.type?.ToString();
1472 string sessionId = message.sessionId?.ToString() ?? ((PuppeteerClient)PuppeteerClient)._currentTargetId;
1473 string url = paramsData.targetInfo?.url?.ToString() ??
"unknown";
1477 if (method.StartsWith(
"Network."))
1480 if (method ==
"Network.requestWillBeSent" ||
1481 method ==
"Network.loadingFinished" ||
1482 method ==
"Network.loadingFailed" ||
1483 method ==
"Network.requestServedFromCache" ||
1484 method ==
"Network.requestWillBeSentExtraInfo")
1486 _events.Enqueue((method, paramsData));
1491 if (
true == CapturingCalls &&
"Network.requestWillBeSent" == method)
1492 RecordCall(paramsData);
1493 else if (
true == CapturingCalls &&
"Network.responseReceived" == method)
1494 RecordStatus(paramsData);
1496 if (method ==
"Network.responseReceived")
1499 string type = paramsData.type?.ToString();
1502 if (type ==
"Document")
1504 this.ServerResponseCode = (int)paramsData.response.status;
1505 string responseUrl = paramsData.response.url?.ToString();
1508 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
1509 $
"HTTP Status Captured: [{this.ServerResponseCode}] for [{responseUrl}]",
1510 this, GPALObjectType.PuppeteerCommunicator);
1515 if (_suppressNetworkEvents)
1519 if (method.StartsWith(
"Cast."))
1523 JObject payload = paramsData;
1524 JToken sink = payload[
"sinks"];
1528 CastDevices.Add(
new CastDevice(sink[0][
"name"]?.ToString(), sink[0][
"id"]?.ToString()));
1531 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
1532 $
"[{method}] [{Newtonsoft.Json.JsonConvert.SerializeObject(paramsData)}]",
1533 this, GPALObjectType.PuppeteerCommunicator);
1538 catch (Exception ex)
1540 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
1541 $
"Error processing [{method}]",
1542 this, GPALObjectType.PuppeteerCommunicator, ex);
1549 if (
"Page.javascriptDialogOpening" == method &&
null != Browser.BrowserSettings.DialogsAccepted)
1551 bool accept =
true == Browser.BrowserSettings.DialogsAccepted;
1552 string typed = Browser.BrowserSettings.DialogText;
1554 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Answering a [{paramsData?.type}] with accept [{accept}] text [{typed}]",
this, GPALObjectType.PuppeteerCommunicator);
1559 _ = SendCommand<object>(DevToolsMethods.PageHandleJavaScriptDialog,
1560 null == typed ? (
object)
new { accept } :
new { accept, promptText = typed },
1561 sessionId, 3,
true);
1564 if (method ==
"Target.targetCreated" && _sendSemaphore.CurrentCount == 1)
1566 if (
true == targetType.Equals(
"page") &&
false == url?.StartsWith(
"chrome://") &&
false == url?.Equals(
"about:blank"))
1568 string targetId = paramsData.targetInfo?.targetId?.ToString();
1569 string browserContextId = paramsData.targetInfo?.browserContextId?.ToString();
1571 if (!
string.IsNullOrEmpty(targetId))
1575 await HandleNewTarget(targetId, browserContextId,
null,
true).ConfigureAwait(
false);
1576 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Detected new uncontrolled tab: TargetId [{targetId}] in Window [{browserContextId}]",
this, GPALObjectType.PuppeteerCommunicator);
1610 GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $
"Received: [{Newtonsoft.Json.JsonConvert.SerializeObject(message)}]",
this, GPALObjectType.PuppeteerCommunicator);
1614 catch (Exception ex)
1616 string innerMsg =
"";
1619 if (ex.InnerException !=
null)
1621 innerMsg = $
" Inner: [{ex.InnerException.Message}]";
1626 string exMsg = $
"Error receiving WebSocket message: [{ex.Message}][{innerMsg}]";
1631 GPAL.PublishSimpleEvent(
1632 GPALEventType.DEEPDEBUG,
1635 GPALObjectType.Puppeteer,
1652 private async Task HandleNewTarget(
string targetId,
string browserContextId,
string newSessionId =
null,
bool doNotGetSendSemaphore =
false)
1654 bool activeTab = !(
null == newSessionId);
1657 await AddTabToQueue(targetId, newSessionId, activeTab, doNotGetSendSemaphore).ConfigureAwait(
false);
1664 if (
true == CapturingCalls)
1665 await
CaptureCalls(
true, newSessionId).ConfigureAwait(
false);
1667 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Handled new target: ID [{targetId}] in Window [{browserContextId}]",
this, GPALObjectType.PuppeteerCommunicator);
1684 var start = DateTime.UtcNow;
1685 while (DateTime.UtcNow - start < timeout)
1687 if (_events.TryDequeue(out var evt) && evt.Event == eventName)
1689 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Waiting for [{eventName}] got [{evt.Event}]",
this, GPALObjectType.PuppeteerCommunicator);
1690 await Task.Delay(100).ConfigureAwait(
false);
1692 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Event [{eventName}] not received within [{timeout.TotalSeconds}]s",
this, GPALObjectType.PuppeteerCommunicator);
1716 var stopwatch = System.Diagnostics.Stopwatch.StartNew();
1719 while (stopwatch.ElapsedMilliseconds < maxWaitMs)
1722 var box = await SendCommand<dynamic>(
1723 DevToolsMethods.DOMGetBoxModel,
1724 new { backendNodeId = element.ElementBackendNodeId },
1726 ).ConfigureAwait(
false);
1728 if (box?.model?.content !=
null)
1730 var quad = box.model.content;
1731 if (quad.Count >= 8 && quad[2] > quad[0] && quad[7] > quad[1])
1736 else if (
null == box)
1738 List<GPALElement> elems = await
EvaluateSelector(element.
Css, sessionId).ConfigureAwait(
false);
1739 if (0 < elems.Count)
1741 element.ElementBackendNodeId = elems[0].ElementBackendNodeId;
1742 element.ElementHandle = elems[0].ElementHandle;
1743 element.ElementNodeId = elems[0].ElementNodeId;
1744 element.Css = elems[0].Css;
1745 element.Xpath = elems[0].Xpath;
1750 var jsResult = await SendCommand<dynamic>(
1751 DevToolsMethods.RuntimeEvaluate,
1756 const el = document.querySelector('{element.Css}') ||
1757 document.evaluate('{element.Xpath}', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
1758 if (!el) return false;
1759 const r = el.getBoundingClientRect();
1760 return r.width > 0.1 && r.height > 0.1 && r.top >= 0; // visible-ish
1762 contextId = CurrentContextId,
1763 returnByValue = true
1766 ).ConfigureAwait(
false);
1768 if (jsResult?.result?.value is
bool visible && visible)
1773 await Task.Delay(pollIntervalMs).ConfigureAwait(
false);
1777 $
"Timeout waiting for layout on [{element.TagName}][{element.Css ?? element.Xpath}] after [{maxWaitMs}]ms",
1778 this, GPALObjectType.PuppeteerCommunicator);
1798 public async Task<string>
CreateTarget(
string url,
bool newWindow =
false,
string sessionId =
null)
1801 url =
MagicHelper.GetFullUrl(url, Browser, out Browser._areRobotsAllowed);
1803 if (
false == Browser.AreRobotsAllowed &&
true == Browser.BrowserSettings.ObeyRobotsTxt)
1805 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Visiting [{url}] is disallowed by robots.txt and your request to honor it. Not going to URL. Workflow will fail.",
this, GPALObjectType.PuppeteerCommunicator);
1806 url =
"https://google.com";
1809 object parameters =
new { url };
1812 var context = await SendCommand<object>(DevToolsMethods.TargetCreateBrowserContext,
new { }, sessionId).ConfigureAwait(
false);
1813 string browserContextId = context.browserContextId.ToString();
1814 parameters =
new { url, browserContextId };
1817 var result = await SendCommand<object>(DevToolsMethods.TargetCreateTarget, parameters,
null).ConfigureAwait(
false);
1818 string targetId = result.targetId.ToString();
1819 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId },
null).ConfigureAwait(
false);
1820 var session = await SendCommand<object>(DevToolsMethods.TargetAttachToTarget,
new { targetId, flatten = true },
null).ConfigureAwait(
false);
1821 string newSessionId = session.sessionId.ToString();
1822 await AddTabToQueue(targetId, newSessionId).ConfigureAwait(
false);
1834 internal async Task CloseOutputWebSocketAsync()
1839 if (
null != _webSocket)
1840 await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure,
"Client closing", CancellationToken.None).ConfigureAwait(
false);
1853 internal async Task<string> GetInitialTargetId(
string sessionId)
1855 dynamic targets = await SendCommand<dynamic>(DevToolsMethods.TargetGetTargets,
new { },
null).ConfigureAwait(
false);
1856 var pageTarget = ((IEnumerable<dynamic>)targets.targetInfos).FirstOrDefault(t => (
string)t.type ==
"page");
1857 return pageTarget !=
null ? (string)pageTarget.targetId : null;
1865 internal async Task<int> TabCount()
1867 dynamic targets = await SendCommand<dynamic>(DevToolsMethods.TargetGetTargets,
new { },
null).ConfigureAwait(
false);
1868 return ((IEnumerable<dynamic>)targets.targetInfos).Count(t => (
string)t.type ==
"page");
1885 if (
string.IsNullOrWhiteSpace(url))
1887 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"URL is empty or null in GetTargetWindowIdByUrl",
this, GPALObjectType.PuppeteerCommunicator);
1891 url =
MagicHelper.GetFullUrl(url, Browser, out
bool _);
1892 var windowArray = _windowSessionsQueue.ToArray();
1893 foreach (var window
in windowArray)
1895 var tabArray = window.TabQueue.ToArray();
1896 foreach (var tab
in tabArray)
1898 var targetInfo = await SendCommand<object>(DevToolsMethods.TargetGetTargetInfo,
new { targetId = tab.Key },
null).ConfigureAwait(
false);
1899 string targetUrl = targetInfo?.targetInfo?.url?.ToString();
1900 if (targetUrl !=
null && targetUrl.Contains(url))
1902 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Found window with browserContextId [{window.BrowserContextId}] for URL [{url}]",
this, GPALObjectType.PuppeteerCommunicator);
1903 return window.BrowserContextId;
1908 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No window found with a tab matching URL [{url}]",
this, GPALObjectType.PuppeteerCommunicator);
1928 if (
string.IsNullOrWhiteSpace(url))
1930 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"URL is empty or null in GetTargetTabIdByUrl",
this, GPALObjectType.PuppeteerCommunicator);
1934 url =
MagicHelper.GetFullUrl(url, Browser, out
bool _);
1935 var windowArray = _windowSessionsQueue.ToArray();
1937 if (_activeWindowIndex >= 0 && _activeWindowIndex < windowArray.Length)
1939 var tabArray = windowArray[_activeWindowIndex].TabQueue.ToArray();
1940 foreach (var tab
in tabArray)
1942 var targetInfo = await SendCommand<object>(DevToolsMethods.TargetGetTargetInfo,
new { targetId = tab.Key },
null).ConfigureAwait(
false);
1943 string targetUrl = targetInfo?.targetInfo?.url?.ToString();
1944 if (targetUrl !=
null)
1946 string decodedTarget = Uri.UnescapeDataString(targetUrl);
1947 string decodedUrl = Uri.UnescapeDataString(url);
1949 if (
true == decodedTarget.Contains(decodedUrl))
1952 $
"Found tab with targetId [{tab.Key}] for URL [{url}] in active window",
1953 this, GPALObjectType.PuppeteerCommunicator);
1961 for (
int i = 0; i < windowArray.Length; i++)
1963 if (i == _activeWindowIndex)
continue;
1964 var tabArray = windowArray[i].TabQueue.ToArray();
1965 foreach (var tab
in tabArray)
1967 var targetInfo = await SendCommand<object>(DevToolsMethods.TargetGetTargetInfo,
new { targetId = tab.Key },
null).ConfigureAwait(
false);
1968 string targetUrl = targetInfo?.targetInfo?.url?.ToString();
1969 if (targetUrl !=
null && targetUrl.Contains(url))
1971 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Found tab with targetId [{tab.Key}] for URL [{url}] in window [{i}]",
this, GPALObjectType.PuppeteerCommunicator);
1977 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No tab found with URL [{url}]",
this, GPALObjectType.PuppeteerCommunicator);
1987 internal async Task<string> GetNextWindowId(
string id,
string sessionId =
null)
1990 var targets = await SendCommand<object>(DevToolsMethods.TargetGetTargets,
new { },
null).ConfigureAwait(
false);
1991 var windowList = ((IEnumerable<dynamic>)targets.targetInfos)
1992 .Where(t => t.type ==
"browser" || t.type ==
"page")
1993 .GroupBy(t => t.browserContextId ?? t.targetId)
1994 .Select(g => g.First())
1995 .OrderBy(t => t.targetId)
1997 for (
int i = 0; i < windowList.Count; i++)
1999 if (windowList[i].targetId.ToString() ==
id)
2001 return i < windowList.Count - 1 ? windowList[i + 1].targetId.ToString() :
null;
2004 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No next window found for ID: [{id}]",
this, GPALObjectType.PuppeteerCommunicator);
2014 internal async Task<string> GetPreviousWindowId(
string id,
string sessionId =
null)
2017 var targets = await SendCommand<object>(DevToolsMethods.TargetGetTargets,
new { },
null).ConfigureAwait(
false);
2018 var windowList = ((IEnumerable<dynamic>)targets.targetInfos)
2019 .Where(t => t.type ==
"browser" || t.type ==
"page")
2020 .GroupBy(t => t.browserContextId ?? t.targetId)
2021 .Select(g => g.First())
2022 .OrderBy(t => t.targetId)
2024 for (
int i = 0; i < windowList.Count; i++)
2026 if (windowList[i].targetId.ToString() ==
id)
2028 return i > 0 ? windowList[i - 1].targetId.ToString() :
null;
2031 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No previous window found for ID: [{id}]",
this, GPALObjectType.PuppeteerCommunicator);
2040 public async Task<int>
QueryByCss(
string selector,
string sessionId =
null)
2045 var doc = await SendCommand<object>(DevToolsMethods.DOMGetDocument,
new { depth = 0 }, sessionId).ConfigureAwait(
false);
2046 var result = await SendCommand<object>(DevToolsMethods.DOMQuerySelector,
new { nodeId = (int)doc.root.nodeId, selector }, sessionId).ConfigureAwait(
false);
2048 var nodeId = (int)result.nodeId;
2051 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No nodes found for Css: [{selector}]",
this, GPALObjectType.PuppeteerCommunicator);
2060 public string ObjectId {
get;
set; }
2068 public int NodeId {
get;
set; }
2071 private readonly SemaphoreSlim _evaluateSelectorSemaphore =
new SemaphoreSlim(1, 1);
2084 CurrentRootObjectId =
null;
2087 var elements = await
EvaluateSelector(frameSelector, isRecursive:
true).ConfigureAwait(
false);
2088 if (elements.Count == 0)
2090 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG,
"Frame not found: " + frameSelector,
this, GPALObjectType.PuppeteerCommunicator);
2094 var iframeElement = elements[0];
2096 var describe = await SendCommand<dynamic>(DevToolsMethods.DOMDescribeNode,
new
2098 objectId = iframeElement.ElementHandle,
2102 string frameId = describe?.node?.frameId?.ToString();
2104 if (
string.IsNullOrEmpty(frameId))
2106 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"No frameId for frame [" + frameSelector +
"]",
this, GPALObjectType.PuppeteerCommunicator);
2110 CurrentFrameId = frameId;
2114 CurrentFrameSessionId =
null;
2119 var attach = await SendCommand<dynamic>(DevToolsMethods.TargetAttachToTarget,
new
2123 },
null).ConfigureAwait(
false);
2125 string frameSessionId = attach?.sessionId?.ToString();
2126 if (!
string.IsNullOrEmpty(frameSessionId))
2127 CurrentFrameSessionId = frameSessionId;
2135 await Task.Delay(300).ConfigureAwait(
false);
2137 var isolated = await SendCommand<dynamic>(DevToolsMethods.PageCreateIsolatedWorld,
new
2140 worldName =
"gpal_isolated_" + Guid.NewGuid().ToString(
"N"),
2141 grantUniversalAccess = false
2146 CurrentContextId = isolated?.executionContextId?.ToObject<
long?>();
2148 if (CurrentContextId.HasValue)
2151 "Resolved iframe [" + frameSelector +
"] | FrameId: " + frameId +
" | ContextId: " + CurrentContextId.Value,
2152 this, GPALObjectType.PuppeteerCommunicator);
2156 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to create isolated world for " + frameSelector,
this, GPALObjectType.PuppeteerCommunicator);
2177 var found = await
EvaluateSelector(elementSelector, isRecursive:
true).ConfigureAwait(
false);
2179 if (0 == found.Count)
2181 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG,
"Element to search inside not found: " + elementSelector,
this, GPALObjectType.PuppeteerCommunicator);
2187 CurrentRootObjectId = found[0].ElementHandle;
2190 "Searching inside [" + elementSelector +
"] | ObjectId: " + CurrentRootObjectId,
2191 this, GPALObjectType.PuppeteerCommunicator);
2194 public async Task SwitchToShadowDom(
string shadowHostSelector)
2196 var hosts = await
EvaluateSelector(shadowHostSelector, isRecursive:
true).ConfigureAwait(
false);
2197 if (hosts.Count == 0)
2199 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG,
"Shadow host not found: " + shadowHostSelector,
this, GPALObjectType.PuppeteerCommunicator);
2203 var host = hosts[0];
2206 var describe = await SendCommand<dynamic>(DevToolsMethods.DOMDescribeNode,
new
2208 objectId = host.ElementHandle,
2211 }, s).ConfigureAwait(
false);
2213 var shadowRoots = describe?.node?.shadowRoots as IEnumerable<dynamic>;
2215 if (shadowRoots?.Any() !=
true)
2217 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG,
"No shadow root on host: " + shadowHostSelector,
this, GPALObjectType.PuppeteerCommunicator);
2221 var shadowRoot = shadowRoots.First();
2223 if (shadowRoot.backendNodeId !=
null)
2225 var resolve = await SendCommand<dynamic>(DevToolsMethods.DOMResolveNode,
new
2227 backendNodeId = (long)shadowRoot.backendNodeId
2228 }, s).ConfigureAwait(
false);
2230 if (resolve?.@
object?.objectId !=
null)
2232 CurrentRootObjectId = (string)resolve.@
object.objectId;
2236 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
2237 "Resolved shadow DOM [" + shadowHostSelector +
"] | ObjectId: " + CurrentRootObjectId,
2238 this, GPALObjectType.PuppeteerCommunicator);
2252 public async Task<List<GPALElement>>
EvaluateSelector(
string selector,
string sessionId =
null,
bool isRecursive =
false)
2254 var elements =
new List<GPALElement>();
2259 string effectiveSessionId = (!
string.IsNullOrEmpty(sessionId) && sessionId != GetCurrentSessionId())
2263 if (
false == isRecursive)
2264 await _evaluateSelectorSemaphore.WaitAsync().ConfigureAwait(
false);
2268 var nodes =
new List<Node>();
2270 bool inRootContext = !
string.IsNullOrEmpty(CurrentRootObjectId);
2274 bool useIsolatedContext = CurrentContextId.HasValue;
2279 "Evaluating inside the scope set by InShadowDom or InElement, for selector [" + selector +
"]",
2280 this, GPALObjectType.PuppeteerCommunicator);
2286 bool isNodeId =
int.TryParse(selector, out
int tmpNodeId);
2287 bool isObjectId = System.Text.RegularExpressions.Regex.IsMatch(selector,
@"^-?\d+\.\d+\.\d+$");
2290 await SendCommand<object>(DevToolsMethods.DOMGetDocument,
new
2294 }, effectiveSessionId).ConfigureAwait(
false);
2297 if (inRootContext && !isNodeId && !isObjectId)
2301 var shadowQueryResult = await SendCommand<dynamic>(DevToolsMethods.RuntimeCallFunctionOn,
new
2303 objectId = CurrentRootObjectId,
2304 functionDeclaration =
@"
2305 function(selector) {
2308 const cssNodes = this.querySelectorAll(selector);
2309 if (cssNodes?.length > 0) {
2310 nodes = Array.from(cssNodes);
2314 if (nodes.length === 0) {
2316 const result = document.evaluate(selector, this, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
2317 for (let i = 0; i < result.snapshotLength; i++) {
2318 const node = result.snapshotItem(i);
2319 if (node?.getBoundingClientRect) {
2320 node.getBoundingClientRect();
2326 return { nodes: nodes };
2328 arguments = new[] { new { value = selector } },
2329 contextId = CurrentContextId,
2330 returnByValue =
false
2331 }, effectiveSessionId).ConfigureAwait(
false);
2333 if (shadowQueryResult?.result?.objectId !=
null)
2336 var wrapper = await SendCommand<dynamic>(DevToolsMethods.RuntimeGetProperties,
2338 objectId = (string)shadowQueryResult.result.objectId,
2339 contextId = CurrentContextId,
2340 ownProperties = true
2341 }, effectiveSessionId).ConfigureAwait(
false);
2343 string nodesArrayId =
null;
2346 if (wrapper?.result !=
null)
2348 foreach (var p
in wrapper.result)
2350 if (p.name ==
"nodes" && p.value?.objectId !=
null)
2352 nodesArrayId = (string)p.value.objectId;
2358 if (!
string.IsNullOrEmpty(nodesArrayId))
2361 var arrayProps = await SendCommand<dynamic>(DevToolsMethods.RuntimeGetProperties,
2363 objectId = nodesArrayId,
2364 contextId = CurrentContextId,
2365 ownProperties = true
2366 }, effectiveSessionId).ConfigureAwait(
false);
2370 if (arrayProps?.result !=
null)
2372 foreach (var item
in arrayProps.result)
2374 var val = item.value;
2376 && val.type ==
"object"
2377 && val.subtype ==
"node"
2378 && val.objectId !=
null)
2380 nodes.Add(
new Node { ObjectId = (string)val.objectId });
2386 if (nodes.Count > 0)
2387 goto ProcessFoundNodes;
2390 catch (Exception ex)
2393 $
"Shadow DOM query failed for [{selector}]. Falling back to main path.",
2394 this, GPALObjectType.PuppeteerCommunicator, ex);
2398 if (isNodeId || isObjectId)
2400 var requestNodeResult = await SendCommand<object>(DevToolsMethods.DOMRequestNode,
new { objectId = selector }, effectiveSessionId).ConfigureAwait(
false);
2401 if (requestNodeResult?.nodeId !=
null)
2403 var nodeId = (int)requestNodeResult.nodeId;
2404 var resolveNodeResult = await SendCommand<object>(DevToolsMethods.DOMResolveNode,
new { nodeId }, effectiveSessionId).ConfigureAwait(
false);
2405 if (resolveNodeResult?.@
object?.objectId !=
null)
2407 var describeNode = await SendCommand<object>(DevToolsMethods.DOMDescribeNode,
new { objectId = (string)resolveNodeResult.@object.objectId, pierce = true }, effectiveSessionId).ConfigureAwait(
false);
2410 ObjectId = (string)resolveNodeResult.@
object.objectId,
2411 BackendNodeId = describeNode?.node?.backendNodeId ?? 0,
2421 function evaluateSelector(selector) {
2423 let type = 'unknown';
2428 const cssNodes = document.querySelectorAll(selector);
2429 if (cssNodes.length > 0) {
2430 nodes = Array.from(cssNodes);
2435 // If CSS failed, try XPath
2436 if (nodes.length === 0) {
2438 const result = document.evaluate(
2442 XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
2446 // Force CDP to wrap each node by accessing a layout property
2447 for (let i = 0; i < result.snapshotLength; i++) {
2448 const node = result.snapshotItem(i);
2449 if (node && node.getBoundingClientRect) {
2450 // This forces CDP to create a RemoteObject with objectId
2451 node.getBoundingClientRect();
2456 if (nodes.length > 0) {
2459 error = 'No elements found for selector [' + selector + ']';
2462 error = 'Invalid XPath or error: ' + e.message;
2466 return { nodes: nodes };
2469 string escapedSelector = selector;
2470 var evalResult = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
new
2472 expression = $
"({jsFunc})(\"{escapedSelector}\")",
2473 contextId = CurrentContextId,
2474 returnByValue = false
2475 }, effectiveSessionId).ConfigureAwait(
false);
2477 if (evalResult?.result?.objectId !=
null)
2479 var nodesProps = await SendCommand<object>(DevToolsMethods.RuntimeGetProperties,
2481 objectId = (string)evalResult.result.objectId,
2482 contextId = CurrentContextId,
2483 ownProperties = true
2484 }, effectiveSessionId).ConfigureAwait(
false);
2485 dynamic errorProp =
null;
2487 if (nodesProps?.result !=
null)
2489 foreach (var p
in nodesProps.result)
2491 if (p.name ==
"error" && p.value !=
null && p.value.value !=
null)
2499 if (errorProp !=
null)
2501 string errMsg = (string)errorProp.value.value ??
"Unknown selector error";
2503 if (
false == isRecursive)
2504 return new List<GPALElement>();
2507 foreach (var prop
in nodesProps.result)
2509 if (prop.name ==
"nodes" && prop.value?.objectId !=
null)
2511 var nodeArray = await SendCommand<object>(DevToolsMethods.RuntimeGetProperties,
2513 objectId = (string)prop.value.objectId,
2514 contextId = CurrentContextId,
2515 ownProperties = true
2516 }, effectiveSessionId).ConfigureAwait(
false);
2517 foreach (var np
in nodeArray.result)
2519 if (np.value !=
null && np.value.type ==
"object" && np.value.subtype ==
"node")
2520 nodes.Add(
new Node { ObjectId = (string)np.value.objectId });
2530 foreach (var node
in nodes)
2534 var attributes =
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
2536 var describeNode = await SendCommand<object>(DevToolsMethods.DOMDescribeNode,
new { objectId = node.ObjectId, pierce = true }, effectiveSessionId).ConfigureAwait(
false);
2537 if (describeNode?.node !=
null)
2539 node.BackendNodeId = describeNode.node.backendNodeId ?? 0;
2540 var requestNodeResult = await SendCommand<object>(DevToolsMethods.DOMRequestNode,
new
2542 objectId = node.ObjectId
2543 }, effectiveSessionId).ConfigureAwait(
false);
2545 node.NodeId = requestNodeResult.nodeId;
2546 attributes[
"tagName"] = describeNode.node.nodeName;
2549 var jsProps = await SendCommand<object>(DevToolsMethods.RuntimeCallFunctionOn,
new
2551 functionDeclaration =
@"
2553 const rect = this.getBoundingClientRect();
2554 const cleanText = (str) => (str || '').replace(/\s+/g, ' ').trim();
2557 enabled: !this.disabled,
2558 selected: this.selected || false,
2559 displayed: this.offsetParent !== null,
2560 href: this.href || '',
2561 src: this.src || '',
2563 value: cleanText(this.value),
2564 text: cleanText(this.text || this.innerText),
2565 placeholder: cleanText(this.placeholder),
2567 index: this.index || '',
2568 length: this.files ? this.files.length : this.length || '',
2569 type: this.type || '',
2572 for (let {name,value} of Array.from(this.attributes||[])) attrs[name]=value;
2574 // Force boolean properties last - multiple not getting set above, being dropped by cdp?
2575 attrs.multiple = this.multiple === true;
2577 function getCss(el){
2578 if(!(el instanceof Element)) return '';
2580 while(el && el.nodeType===1){
2581 let part=el.nodeName.toLowerCase();
2582 if(el.id){part+='#'+el.id; parts.unshift(part); break;}
2584 while((sib=sib.previousElementSibling)!=null){if(sib.nodeName.toLowerCase()===part)nth++;}
2585 if(nth>1) part+=':nth-of-type('+nth+')';
2586 parts.unshift(part); el=el.parentElement;
2588 return parts.join(' > ');
2590 return {attributes:attrs,css:getCss(this),boundingRect:{x:rect.x,y:rect.y,width:rect.width,height:rect.height,top:rect.top,left:rect.left,bottom:rect.bottom,right:rect.right}};
2592 objectId = node.ObjectId,
2593 contextId = CurrentContextId,
2594 returnByValue = true
2595 }, effectiveSessionId).ConfigureAwait(
false);
2597 if (jsProps?.result?.value !=
null)
2599 var dict = ((Newtonsoft.Json.Linq.JObject)jsProps.result.value).ToObject<Dictionary<string, object>>();
2601 if (dict.ContainsKey(
"attributes"))
2603 var jsAttrDict = ((Newtonsoft.Json.Linq.JObject)dict[
"attributes"]).ToObject<Dictionary<string, object>>();
2604 foreach (var kvp
in jsAttrDict)
if (!attributes.ContainsKey(kvp.Key)) attributes[kvp.Key] = kvp.Value;
2607 if (dict.ContainsKey(
"boundingRect"))
2609 var brObj = ((Newtonsoft.Json.Linq.JObject)dict[
"boundingRect"]).ToObject<Dictionary<string, object>>();
2610 float ToFloat(
object o)
2612 if (o is
double d)
return (
float)d;
2613 if (o is
long l)
return (
float)l;
2614 if (o is
int i)
return (
float)i;
2615 if (o is
string s &&
float.TryParse(s, out var r))
return r;
2621 X = ToFloat(brObj.ContainsKey(
"x") ? brObj[
"x"] : 0),
2622 Y = ToFloat(brObj.ContainsKey(
"y") ? brObj[
"y"] : 0),
2623 Width = ToFloat(brObj.ContainsKey(
"width") ? brObj[
"width"] : 0),
2624 Height = ToFloat(brObj.ContainsKey(
"height") ? brObj[
"height"] : 0),
2625 Top = ToFloat(brObj.ContainsKey(
"top") ? brObj[
"top"] : 0),
2626 Left = ToFloat(brObj.ContainsKey(
"left") ? brObj[
"left"] : 0),
2627 Bottom = ToFloat(brObj.ContainsKey(
"bottom") ? brObj[
"bottom"] : 0),
2628 Right = ToFloat(brObj.ContainsKey(
"right") ? brObj[
"right"] : 0)
2631 attributes[
"boundingRect"] = rect;
2632 attributes[
"location"] =
new System.Drawing.Point((
int)rect.X, (
int)rect.Y);
2633 attributes[
"size"] =
new System.Drawing.
Size((
int)rect.Width, (
int)rect.Height);
2636 if (dict.ContainsKey(
"css")) attributes[
"css"] = dict[
"css"]?.ToString();
2639 attributes[
"xpath"] = await
GetXPathForElement(node.BackendNodeId).ConfigureAwait(
false);
2643 ElementHandle = node.ObjectId,
2644 ElementBackendNodeId = node.BackendNodeId,
2645 ElementNodeId = node.NodeId
2676 return new List<GPALElement>();
2681 if (
false == isRecursive)
2682 _evaluateSelectorSemaphore.Release();
2694 public async Task<string>
GetCssForElement(
int backendNodeId,
bool optimized =
false,
string sessionId =
null)
2698 function getCssSelector(node) {
2699// console.log('getCssSelector called with node:', node);
2701 if (!node || node.nodeType !== Node.ELEMENT_NODE) {
2702 console.error('Invalid node, returning empty string');
2707// console.log('Found ID:', node.id);
2708 return '#' + node.id.replace(/([ #.;+*~'!^$[\]()=>|\/@])/g, '\\$1');
2711 let selector = node.localName.toLowerCase();
2712// console.log('Tag name:', selector);
2714 if (node.classList && node.classList.length > 0) {
2715 const classes = Array.from(node.classList)
2716 .filter(c => c && /^[a-zA-Z][\w-]*$/.test(c))
2717 .map(c => c.replace(/([ #.;+*~'!^$[\]()=>|\/@])/g, '\\$1'));
2718 if (classes.length > 0) {
2719 selector += '.' + classes.join('.');
2721 // console.log('Selector with classes:', selector);
2725 const matches = document.querySelectorAll(selector);
2726 // console.log('Matches for selector:', matches.length, selector);
2727 if (matches.length === 1 && matches[0] === node) {
2731 // console.error('Error in querySelectorAll:', e.message);
2735 // console.log('Returning fallback selector:', selector);
2747 public async Task<string>
GetXPathForElement(
int backendNodeId,
bool optimized =
true,
string sessionId =
null)
2752 function getXPathForElement(node, optimized) {
2753 let Elements = { DOMPath: {} };
2755 Elements.DOMPath.xPath = function(node, optimized) {
2756 if (node.nodeType === Node.DOCUMENT_NODE) return '/';
2758 let contextNode = node;
2759 while (contextNode) {
2760 const step = Elements.DOMPath._xPathValue(contextNode, optimized);
2763 if (step.optimized) break;
2764 contextNode = contextNode.parentNode;
2767 return (steps.length && steps[0].optimized ? '' : '/') + steps.map(step => step.value).join('/');
2770 Elements.DOMPath._xPathValue = function(node, optimized) {
2772 const ownIndex = Elements.DOMPath._xPathIndex(node);
2773 if (ownIndex === -1) return null;
2774 switch (node.nodeType) {
2775 case Node.ELEMENT_NODE:
2776 if (optimized && node.getAttribute('id'))
2777 return new Elements.DOMPath.Step('//*[@id=\'' + node.getAttribute('id') + '\']', true);
2778 ownValue = node.localName;
2780 case Node.ATTRIBUTE_NODE:
2781 ownValue = '@' + node.nodeName;
2783 case Node.TEXT_NODE:
2784 case Node.CDATA_SECTION_NODE:
2785 ownValue = 'text()';
2787 case Node.PROCESSING_INSTRUCTION_NODE:
2788 ownValue = 'processing-instruction()';
2790 case Node.COMMENT_NODE:
2791 ownValue = 'comment()';
2793 case Node.DOCUMENT_NODE:
2801 ownValue += '[' + ownIndex + ']';
2802 return new Elements.DOMPath.Step(ownValue, node.nodeType === Node.DOCUMENT_NODE);
2805 Elements.DOMPath._xPathIndex = function(node) {
2806 function areNodesSimilar(left, right) {
2807 if (left === right) return true;
2808 if (left.nodeType === Node.ELEMENT_NODE && right.nodeType === Node.ELEMENT_NODE)
2809 return left.localName === right.localName;
2810 if (left.nodeType === right.nodeType) return true;
2811 const leftType = left.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left.nodeType;
2812 const rightType = right.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right.nodeType;
2813 return leftType === rightType;
2815 const siblings = node.parentNode ? node.parentNode.children : null;
2816 if (!siblings) return 0;
2817 let hasSameNamedElements;
2818 for (let i = 0; i < siblings.length; ++i) {
2819 if (areNodesSimilar(node, siblings[i]) && siblings[i] !== node) {
2820 hasSameNamedElements = true;
2824 if (!hasSameNamedElements) return 0;
2826 for (let i = 0; i < siblings.length; ++i) {
2827 if (areNodesSimilar(node, siblings[i])) {
2828 if (siblings[i] === node) return ownIndex;
2835 Elements.DOMPath.Step = function(value, optimized) {
2837 this.optimized = optimized || false;
2840 return Elements.DOMPath.xPath(node, optimized);
2854 public async Task<string>
GetXPathOrCssForElement(
int backendNodeId,
string jsFunc,
bool optimized,
string sessionId)
2859 var resolve = await SendCommand<dynamic>(DevToolsMethods.DOMResolveNode,
new { backendNodeId, pierce = true }, sessionId).ConfigureAwait(
false);
2860 string objectId = resolve.@
object.objectId;
2863 var result = await SendCommand<dynamic>(DevToolsMethods.RuntimeCallFunctionOn,
new
2866 functionDeclaration = $
"function(optimized) {{ return ({jsFunc})(this, optimized); }}",
2867 arguments = new object[] { new { value = optimized } },
2868 contextId = CurrentContextId,
2869 returnByValue =
true
2870 }, sessionId).ConfigureAwait(
false);
2875 return result?.result?.value?.ToString() ??
"";
2877 catch (Exception ex)
2880 $
"Failed to get XPath for backendNodeId [{backendNodeId}]",
2881 this, GPALObjectType.PuppeteerCommunicator, ex);
2887 static Dictionary<string, string> GetAttributesAsDictionary(
string jsonString)
2891 using System.Text.Json.JsonDocument doc = System.Text.Json.JsonDocument.Parse(jsonString);
2894 if (!doc.RootElement.TryGetProperty(
"node", out System.Text.Json.JsonElement nodeElement))
2900 if (!nodeElement.TryGetProperty(
"attributes", out System.Text.Json.JsonElement attributesElement) ||
2901 attributesElement.ValueKind != System.Text.Json.JsonValueKind.Array)
2906 var dict =
new Dictionary<string, string>();
2907 for (
int i = 0; i < attributesElement.GetArrayLength() - 1; i += 2)
2909 string key = attributesElement[i].GetString();
2910 string value = attributesElement[i + 1].GetString();
2916 catch (System.Text.Json.JsonException)
2926 public async Task LeftClickByCss(
string selector,
string sessionId =
null)
2928 List<GPALElement> elems = await EvaluateSelector(selector, sessionId).ConfigureAwait(
false);
2929 List<dynamic> responses =
new List<dynamic>();
2932 await ClickElement(elem, sessionId, responses, ClickType.LeftClick).ConfigureAwait(
false);
2941 public async Task<dynamic>
ExecuteJavaScript(
string expression,
string sessionId =
null)
2944 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
2947 contextId = CurrentContextId,
2948 returnByValue = true,
2951 , sessionId).ConfigureAwait(
false);
2965 internal string LastNavigationError {
get;
set; }
2988 public async Task
CaptureCalls(
bool capture,
string sessionId =
null)
2990 CapturingCalls = capture;
2996 if (
true == capture)
2997 foreach (
string session
in KnownSessions(sessionId))
2998 await SendCommand<object>(DevToolsMethods.NetworkEnable,
new { }, session).ConfigureAwait(
false);
3002 private List<string> KnownSessions(
string sessionId)
3004 List<string> retVal =
new List<string>();
3006 foreach (
string session
in new[] { sessionId, GetEffectiveSessionId() })
3007 if (
false ==
string.IsNullOrEmpty(session) &&
false == retVal.Contains(session))
3008 retVal.Add(session);
3010 foreach (WindowSession window
in _windowSessionsQueue.ToArray())
3011 foreach (KeyValuePair<string, string> tab
in window.TabQueue.ToArray())
3012 if (
false ==
string.IsNullOrEmpty(tab.Value) &&
false == retVal.Contains(tab.Value))
3013 retVal.Add(tab.Value);
3019 private void RecordStatus(dynamic paramsData)
3023 string requestId = paramsData?.requestId?.ToString();
3025 if (
false ==
string.IsNullOrEmpty(requestId) &&
true == _capturedById.TryGetValue(requestId, out GPALCall call))
3026 call.Status = (int)(paramsData?.response?.status ?? 0);
3028 catch (Exception ex)
3030 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Could not record a response status",
this, GPALObjectType.PuppeteerCommunicator, ex);
3035 private void RecordCall(dynamic paramsData)
3039 GPALCall call =
new GPALCall
3041 Url = paramsData?.request?.url?.ToString(),
3042 Method = paramsData?.request?.method?.ToString(),
3043 ResourceType = paramsData?.type?.ToString(),
3044 PostData = paramsData?.request?.postData?.ToString(),
3045 Initiator = paramsData?.initiator?.type?.ToString()
3049 if (paramsData?.request?.headers is Newtonsoft.Json.Linq.JObject headers)
3050 foreach (KeyValuePair<string, Newtonsoft.Json.Linq.JToken> header
in headers)
3051 call.Headers[header.Key] = header.Value?.ToString();
3055 string filter = Browser?.BrowserSettings?.CallFilter;
3057 if (
false ==
string.IsNullOrEmpty(filter) &&
true != call.Url?.Contains(filter))
3060 call.RequestId = paramsData?.requestId?.ToString();
3062 if (
false ==
string.IsNullOrEmpty(call.RequestId))
3063 _capturedById[call.RequestId] = call;
3065 CapturedCalls.Add(call);
3067 catch (Exception ex)
3069 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Could not record a request",
this, GPALObjectType.PuppeteerCommunicator, ex);
3073 internal async Task SetExtraHttpHeaders(Dictionary<string, object> headers,
string sessionId =
null)
3075 sessionId ??= GetEffectiveSessionId();
3079 await SendCommand<object>(DevToolsMethods.NetworkEnable,
new { }, sessionId).ConfigureAwait(
false);
3080 await SendCommand<object>(DevToolsMethods.NetworkSetExtraHTTPHeaders,
new { headers }, sessionId).ConfigureAwait(
false);
3083 public async Task<bool> NavigateToUrl(
string url,
string sessionId)
3085 _attachedFilesPerSession.Clear();
3086 var result = await SendCommand<object>(DevToolsMethods.PageNavigate,
new { url }, sessionId).ConfigureAwait(
false);
3090 LastNavigationError = (result as Newtonsoft.Json.Linq.JObject)?[
"errorText"]?.ToString();
3093 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to Go to [{url}]",
this, GPALObjectType.PuppeteerCommunicator);
3094 else if (
false ==
string.IsNullOrEmpty(LastNavigationError))
3095 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to Go to [{url}][{LastNavigationError}]",
this, GPALObjectType.PuppeteerCommunicator);
3098 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Go to [{url}]",
this, GPALObjectType.PuppeteerCommunicator);
3100 return null != result &&
true ==
string.IsNullOrEmpty(LastNavigationError);
3114 public async Task<dynamic>
Back(
string sessionId =
null)
3117 dynamic history = await SendCommand<object>(DevToolsMethods.PageGetNavigationHistory,
new { }, sessionId).ConfigureAwait(
false);
3118 if (history?.currentIndex !=
null)
3120 var entries = (IEnumerable<dynamic>)history.entries;
3121 var nextEntry = entries.ElementAtOrDefault((
int)history.currentIndex - 1);
3122 if (nextEntry?.
id !=
null)
3124 var result = await SendCommand(DevToolsMethods.PageNavigateToHistoryEntry,
new { entryId = (int)nextEntry.id }, sessionId).ConfigureAwait(
false);
3128 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No previous page in history",
this, GPALObjectType.PuppeteerClient);
3141 return await SendCommand<object>(DevToolsMethods.PageCaptureScreenshot,
new { format =
"png" }, sessionId).ConfigureAwait(
false);
3156 public async Task
CastDesktop(
string sinkName,
string sessionId)
3158 CastDevice sink =
null;
3162 await SendCommand<object>(DevToolsMethods.CastDisable,
null, sessionId).ConfigureAwait(
false);
3164 while (
null == sink && 0 < retries--)
3166 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Searching for [{sinkName}].",
this, GPALObjectType.PuppeteerCommunicator);
3167 await SendCommand<object>(DevToolsMethods.CastEnable,
new { }, sessionId).ConfigureAwait(
false);
3169 await Task.Delay(3_000).ConfigureAwait(
false);
3171 sink = CastDevices.Find(device => device.name == sinkName);
3174 await Task.Delay(3_000).ConfigureAwait(
false);
3178 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"[{sinkName}] found.",
this, GPALObjectType.PuppeteerCommunicator);
3179 SinkName = sinkName;
3180 object param =
new { sinkName = sink.name };
3183 await SendCommand<object>(DevToolsMethods.CastStartDesktopMirroring, param, sessionId).ConfigureAwait(
false);
3186 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"[{sinkName}] NOT found.",
this, GPALObjectType.PuppeteerCommunicator);
3201 public async Task
CastTab(
string sinkName,
string sessionId)
3203 CastDevice sink =
null;
3207 await SendCommand<object>(DevToolsMethods.CastDisable,
null, sessionId).ConfigureAwait(
false);
3209 while (
null == sink && 0 < retries--)
3211 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Searching for [{sinkName}].",
this, GPALObjectType.PuppeteerCommunicator);
3212 await SendCommand<object>(DevToolsMethods.CastEnable,
new { }, sessionId).ConfigureAwait(
false);
3214 await Task.Delay(3_000).ConfigureAwait(
false);
3216 sink = CastDevices.Find(device => device.name == sinkName);
3219 await Task.Delay(3_000).ConfigureAwait(
false);
3223 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"[{sinkName}] found.",
this, GPALObjectType.PuppeteerCommunicator);
3224 SinkName = sinkName;
3225 object param =
new { sinkName = sink.name };
3228 await SendCommand<object>(DevToolsMethods.CastStartTabMirroring, param, sessionId).ConfigureAwait(
false);
3231 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"[{sinkName}] NOT found.",
this, GPALObjectType.PuppeteerCommunicator);
3246 public async Task<bool>
StopCasting(
string sessionId)
3251 await SendCommand<object>(DevToolsMethods.CastStopCasting,
new { sinkName = SinkName }, sessionId).ConfigureAwait(
false);
3272 public async Task<bool>
CheckNetworkIdle(
string sessionId =
null,
int maxConnections = 0,
int timeoutMs = 30000,
int pruneMs = 3000,
string sessionToken =
null)
3274 bool retVal =
false;
3275 GPALEventType consoleTypeSave =
GPAL.GPALSettings.ConsoleEvents;
3276 GPALEventType debugTypeSave =
GPAL.GPALSettings.DebugEvents;
3277 string currentErrorMessage =
null;
3280 if (lastSessionToken != sessionToken)
3282 lastErrorMessage.Clear();
3283 supressedMessage =
false;
3284 lastSessionToken = sessionToken;
3287 currentErrorMessage = $
"Waiting up to [{timeoutMs}] ms for network to idle to [{maxConnections}] connections for [{pruneMs}] ms";
3289 GPAL.
PublishSimpleEvent(GPALEventType.INFO, currentErrorMessage,
this, GPALObjectType.PuppeteerCommunicator);
3293 _suppressNetworkEvents =
true;
3294 await SendCommand(DevToolsMethods.NetworkEnable,
new { cacheDisabled = true }, sessionId).ConfigureAwait(
false);
3295 var start = DateTime.UtcNow;
3296 DateTime? lastRequestTime =
null;
3297 var pendingEvents =
new List<(string Event, dynamic Data, DateTime Timestamp)>();
3298 int unmatchedCount = 0;
3299 bool isFirstIteration =
true;
3303 await Task.Delay(500).ConfigureAwait(
false);
3306 while (DateTime.UtcNow - start < TimeSpan.FromMilliseconds(timeoutMs))
3309 while (_events.TryDequeue(out (
string Event, dynamic Data) evt))
3311 pendingEvents.Add((evt.Event, evt.Data, DateTime.UtcNow));
3315 pendingEvents.Sort((a, b) => a.Timestamp.CompareTo(b.Timestamp));
3318 foreach (var evt
in pendingEvents)
3320 string requestId = evt.Data?.requestId?.ToString() ??
"unknown";
3321 string url = evt.Data?.url?.ToString() ??
"unknown";
3322 string resourceType = evt.Data?.resourceType?.ToString() ??
"unknown";
3323 string initiator = evt.Data?.initiator?.type?.ToString() ??
"unknown";
3324 string frameId = evt.Data?.frameId?.ToString() ??
"unknown";
3325 string loaderId = evt.Data?.loaderId?.ToString() ??
"unknown";
3326 string dataJson = evt.Data !=
null ? Newtonsoft.Json.JsonConvert.SerializeObject(evt.Data) :
"null";
3328 if (evt.Event ==
"Network.requestWillBeSent")
3330 lastRequestTime = DateTime.UtcNow;
3331 GPAL.
PublishSimpleEvent(GPALEventType.DEEPDEBUG, $
"[{(isFirstIteration ? "Initial
" : "")}] Request started: requestId=[{requestId}], url=[{url}], resourceType=[{resourceType}], initiator=[{initiator}], frameId=[{frameId}], loaderId=[{loaderId}], data=[{dataJson}]",
this, GPALObjectType.PuppeteerCommunicator);
3333 else if (evt.Event ==
"Network.requestWillBeSentExtraInfo")
3335 string associatedRequestId = evt.Data?.associatedRequestId?.ToString();
3336 GPAL.
PublishSimpleEvent(GPALEventType.DEEPDEBUG, $
"[{(isFirstIteration ? "Initial
" : "")}] Redirected request: requestId=[{requestId}], associatedRequestId=[{associatedRequestId}], url=[{url}], resourceType=[{resourceType}], initiator=[{initiator}], frameId=[{frameId}], loaderId=[{loaderId}], data=[{dataJson}]",
this, GPALObjectType.PuppeteerCommunicator);
3338 else if (evt.Event ==
"Network.loadingFinished" || evt.Event ==
"Network.loadingFailed")
3341 if (unmatchedCount % 10 == 0)
3343 GPAL.
PublishSimpleEvent(GPALEventType.DEEPDEBUG, $
"No matching request for [{evt.Event}]: requestId=[{requestId}], url=[{url}], resourceType=[{resourceType}], initiator=[{initiator},] frameId=[{frameId}], loaderId=[{loaderId}], data=[{dataJson}], unmatchedCount=[{unmatchedCount}]",
this, GPALObjectType.PuppeteerCommunicator);
3346 else if (evt.Event ==
"Network.requestServedFromCache")
3348 GPAL.
PublishSimpleEvent(GPALEventType.DEEPDEBUG, $
"[{(isFirstIteration ? "Initial
" : "")}] Cached request: requestId=[{requestId}], url=[{url}], resourceType=[{resourceType}], initiator=[{initiator}], frameId=[{frameId}], loaderId=[{loaderId}], data=[{dataJson}], activeConnections=0",
this, GPALObjectType.PuppeteerCommunicator);
3350 else if (isFirstIteration)
3352 GPAL.
PublishSimpleEvent(GPALEventType.DEEPDEBUG, $
"Discarded initial event: [{evt.Event}], requestId=[{requestId}], url=[{url}], resourceType=[{resourceType}], initiator=[{initiator}], frameId=[{frameId}], loaderId=[{loaderId}], data=[{dataJson}]",
this, GPALObjectType.PuppeteerCommunicator);
3355 pendingEvents.Clear();
3358 if (lastRequestTime ==
null || (DateTime.UtcNow - lastRequestTime >= TimeSpan.FromMilliseconds(pruneMs)))
3361 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Network idle achieved: no new requests for [{pruneMs}]ms",
this, GPALObjectType.PuppeteerCommunicator);
3366 if (isFirstIteration)
3368 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Initial queue processed: unmatchedCount=[{unmatchedCount}]",
this, GPALObjectType.PuppeteerCommunicator);
3369 isFirstIteration =
false;
3372 await Task.Delay(50, _cts.Token).ConfigureAwait(
false);
3376 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"CheckNetworkIdle complete: unmatchedCount=[{unmatchedCount}], result=[{retVal}]",
this, GPALObjectType.PuppeteerCommunicator);
3378 catch (Exception ex)
3380 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to check network idle",
null, GPALObjectType.PuppeteerCommunicator, ex);
3388 await SendCommand(DevToolsMethods.NetworkDisable,
new { }, sessionId).ConfigureAwait(
false);
3390 _suppressNetworkEvents =
false;
3392 currentErrorMessage = $
"Network IS{(retVal ? "" : " NOT
")} Idle. Status [{retVal}]";
3394 if (
false == lastErrorMessage.Contains(currentErrorMessage))
3396 lastErrorMessage.Add(currentErrorMessage);
3397 GPAL.
PublishSimpleEvent(GPALEventType.INFO, currentErrorMessage,
this, GPALObjectType.PuppeteerCommunicator);
3398 supressedMessage =
false;
3400 else if (
false == supressedMessage)
3402 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", currentErrorMessage, GPALObjectType.Other);
3403 supressedMessage =
true;
3414 public async Task ClearReferrer(
string sessionId =
null)
3416 sessionId ??= GetEffectiveSessionId();
3417 await SendCommand<object>(DevToolsMethods.NetworkSetExtraHTTPHeaders,
new { headers = new { Referer =
"" } }, sessionId).ConfigureAwait(
false);
3425 private void CheckForLastBrowserWindowOpen(
string reason)
3427 if (_windowSessionsQueue.IsEmpty)
3429 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"[{reason}] : Last window closed, **BE CAREFUL AND DO NOT** try to run browser commands until another browser is opened.",
this, GPALObjectType.PuppeteerCommunicator);
3444 public async Task<bool>
CloseTab(
string url =
null,
string tabId =
null,
string sessionId =
null)
3448 return await
CloseTab(targetId, sessionId).ConfigureAwait(
false);
3457 public async Task<bool>
CloseTab(
string targetId,
string sessionId =
null)
3459 bool retVal =
false;
3460 if (
string.IsNullOrEmpty(targetId))
3462 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No targetId provided for CloseTab",
this, GPALObjectType.PuppeteerCommunicator);
3467 await SendCommand<object>(DevToolsMethods.TargetCloseTarget,
new { targetId },
null).ConfigureAwait(
false);
3468 bool removed = RemoveTabFromQueue(targetId);
3471 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Failed to remove tab [{targetId}] from queue",
this, GPALObjectType.PuppeteerCommunicator);
3474 var windowArray = _windowSessionsQueue.ToArray();
3475 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
3477 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"No active window after closing tab [{targetId}]",
this, GPALObjectType.PuppeteerCommunicator);
3482 var tabQueue = windowArray[_activeWindowIndex].TabQueue;
3483 if (tabQueue !=
null && tabQueue.IsEmpty)
3485 string browserContextId = windowArray[_activeWindowIndex].BrowserContextId;
3486 await
CloseWindow(browserContextId, sessionId).ConfigureAwait(
false);
3487 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Closed window for targetId [{targetId}] (context [{browserContextId}]) as it has no tabs",
this, GPALObjectType.PuppeteerCommunicator);
3492 CheckForLastBrowserWindowOpen(
"CloseTab()");
3507 public async Task<bool>
CloseWindow(
string browserContextId,
string sessionId =
null)
3509 bool retVal =
false;
3512 var windowArray = _windowSessionsQueue.ToArray();
3513 var window = windowArray.FirstOrDefault(w => w.BrowserContextId == browserContextId);
3516 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Window with browserContextId [{browserContextId}] not found",
this, GPALObjectType.PuppeteerCommunicator);
3521 var tabArray = window.TabQueue.ToArray();
3522 foreach (var tab
in tabArray)
3524 await SendCommand<object>(DevToolsMethods.TargetCloseTarget,
new { targetId = tab.Key },
null).ConfigureAwait(
false);
3525 RemoveTabFromQueue(tab.Key);
3528 if (RemoveWindowFromQueue(browserContextId))
3530 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Closed window with browserContextId [{browserContextId}]",
this, GPALObjectType.PuppeteerCommunicator);
3535 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to remove window with browserContextId [{browserContextId}]",
this, GPALObjectType.PuppeteerCommunicator);
3539 catch (Exception ex)
3541 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to close window with browserContextId [{browserContextId}]",
this, GPALObjectType.PuppeteerCommunicator, ex);
3544 CheckForLastBrowserWindowOpen(
"CloseWindow()");
3574 string domain =
null,
3575 string storeName =
null,
3580 int deletedCount = 0;
3581 bool retVal =
false;
3583 bool IsMissing(
string s) =>
string.IsNullOrEmpty(s);
3587 string origin =
false ==
string.IsNullOrEmpty(domain) ? UrlHelper.GetOrigin(domain) : UrlHelper.GetOrigin(
BrowserHelper.
GetCurrentUrl(Browser.BrowserSettings));
3589 if (IsMissing(domain))
3590 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"DOMAIN IS MISSING. Deleting all [{storageType}] for origin [{origin}].",
this, GPALObjectType.PuppeteerCommunicator);
3592 switch (storageType)
3596 var cookiesJson = await
GetStorage(
"cookie", sessionId, domain, storeName, path, key).ConfigureAwait(
false);
3597 var cookies = JsonConvert.DeserializeObject<List<dynamic>>(cookiesJson) ??
new List<dynamic>();
3598 foreach (dynamic c
in cookies)
3600 var delParams =
new Dictionary<string, object>
3602 {
"name", (string)c.Key },
3603 {
"domain", (string)c.Domain },
3605 {
"path", (string)c.Path },
3609 if (c.PartitionKey !=
null)
3611 var pk =
new Dictionary<string, object>
3613 {
"topLevelSite", c.PartitionKey.topLevelSite },
3614 {
"hasCrossSiteAncestor", c.PartitionKey.hasCrossSiteAncestor }
3617 delParams.Add(
"partitionKey", pk);
3619 retVal = await SendCommand(DevToolsMethods.NetworkDeleteCookies, delParams, sessionId).ConfigureAwait(
false);
3625 case "localStorage":
3626 case "sessionStorage":
3628 if (IsMissing(domain))
3630 string js =
$@"(function() {{
3632 {storageType}.clear();
3639 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
3642 awaitPromise = false,
3643 contextId = CurrentContextId,
3644 returnByValue = true
3645 }, sessionId).ConfigureAwait(
false).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3655 var keysJson = await
GetStorage(storageType, sessionId, domain).ConfigureAwait(
false);
3656 var keys = JsonConvert.DeserializeObject<List<string>>(keysJson) ??
new List<string>();
3657 foreach (var k
in keys)
3659 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
3661 expression = $
"{storageType}.removeItem('{EscapeJsString(k)}');",
3662 awaitPromise = false,
3663 contextId = CurrentContextId,
3664 returnByValue = true
3665 }, sessionId).ConfigureAwait(
false).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3672 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
3674 expression = $
"{storageType}.removeItem('{EscapeJsString(key)}');",
3675 awaitPromise = false,
3676 contextId = CurrentContextId,
3677 returnByValue = true
3678 }, sessionId).ConfigureAwait(
false).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3686 if (IsMissing(domain))
3688 retVal = SendCommand<dynamic>(DevToolsMethods.StorageClearDataForOrigin,
new
3691 storageTypes =
"indexeddb"
3692 }, sessionId).GetAwaiter().GetResult() !=
null;
3700 if (IsMissing(storeName))
3703 var dbsJson = await
GetStorage(
"indexeddb", sessionId, domain).ConfigureAwait(
false);
3704 var dbs = JsonConvert.DeserializeObject<List<string>>(dbsJson) ??
new List<string>();
3705 foreach (var db
in dbs)
3707 string js =
$@"(function(){{ indexedDB.deleteDatabase('{EscapeJsString(db)}'); }})();";
3708 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
3711 awaitPromise = true,
3712 contextId = CurrentContextId,
3713 returnByValue = true
3714 }, sessionId).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3719 else if (IsMissing(key))
3721 string js =
$@"(function(){{ indexedDB.deleteDatabase('{EscapeJsString(storeName)}'); }})();";
3722 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
3725 awaitPromise = true,
3726 contextId = CurrentContextId,
3727 returnByValue = true
3728 }, sessionId).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3734 string js =
$@"(function() {{
3735 return new Promise((resolve, reject) => {{
3736 const dbName = '{EscapeJsString(path)}'; // path = dbName (per your note)
3737 const store = '{EscapeJsString(storeName)}'; // storeName = store (per your note)
3738 const recordKey = '{EscapeJsString(key)}';
3740 const openReq = indexedDB.open(dbName);
3742 openReq.onerror = () => reject(openReq.error);
3744 openReq.onsuccess = () => {{
3745 const db = openReq.result;
3747 const tx = db.transaction(store, 'readwrite');
3748 const objectStore = tx.objectStore(store);
3750 objectStore.delete(recordKey);
3752 tx.oncomplete = () => {{
3754 resolve(true); // so your .Contains(""true"") check still works
3757 tx.onerror = () => {{
3766 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
3770 awaitPromise = true,
3771 contextId = CurrentContextId,
3772 returnByValue = true
3773 }, sessionId).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3783 if (IsMissing(domain))
3785 string js =
@"(async function() {
3787 const keys = await caches.keys();
3788 await Promise.all(keys.map(k => caches.delete(k)));
3795 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
3798 awaitPromise = true,
3799 contextId = CurrentContextId,
3800 returnByValue = true
3801 }, sessionId).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3811 var cachesJson = await
GetStorage(
"cache", sessionId, domain).ConfigureAwait(
false);
3812 var caches = JsonConvert.DeserializeObject<List<string>>(cachesJson) ??
new List<string>();
3813 foreach (var c
in caches)
3815 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
3817 expression = $
"caches.open('{EscapeJsString(path)}').then(cache => cache.delete('{EscapeJsString(c)}',{{ignoreVary: true}}));",
3818 awaitPromise = true,
3819 contextId = CurrentContextId,
3820 returnByValue = true
3821 }, sessionId).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3828 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
3830 expression = $
"caches.open('{EscapeJsString(path)}').then(cache => cache.delete('{EscapeJsString(key)}', {{ignoreVary: true}}));",
3831 awaitPromise = true,
3832 contextId = CurrentContextId,
3833 returnByValue = true
3834 }, sessionId).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3843 GPALEventType.ERROR,
3844 $
"Unsupported storage type: [{storageType}]",
3846 GPALObjectType.PuppeteerCommunicator);
3850 catch (Exception ex)
3853 GPALEventType.EXCEPTION,
3854 $
"Failed for [{storageType}] d[{domain}] sn[{storeName}] p[{path}] k[{key}]",
3856 GPALObjectType.PuppeteerCommunicator,
3860 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Deleted [{deletedCount}] [{storageType}](s)",
this, GPALObjectType.PuppeteerCommunicator);
3862 return 0 < deletedCount;
3871 private static string EscapeJsString(
string value)
3873 if (
string.IsNullOrEmpty(value))
return "";
3874 return value.Replace(
"\\",
"\\\\")
3875 .Replace(
"'",
"\\'")
3876 .Replace(
"\r",
"\\r")
3877 .Replace(
"\n",
"\\n");
3892 public async Task<dynamic> Evaluate(
string xpath,
string sessionId =
null)
3894 sessionId ??= GetEffectiveSessionId();
3895 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
3897 contextId = CurrentContextId,
3898 expression = $
"document.evaluate('{xpath}', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue"
3899 }, sessionId).ConfigureAwait(
false);
3914 public async Task<dynamic> EvaluateAll(
string xpath,
string sessionId =
null)
3916 sessionId ??= GetEffectiveSessionId();
3917 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
3919 contextId = CurrentContextId,
3920 expression = $
"Array.from(document.evaluate('{xpath}', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null))"
3921 }, sessionId).ConfigureAwait(
false);
3930 private string GetKeyCode(
char c)
3932 char upper =
char.ToUpperInvariant(c);
3934 if (upper >=
'A' && upper <=
'Z')
3936 return "Key" + upper;
3939 if (
char.IsDigit(c))
3960 return "BracketLeft";
3964 return "BracketRight";
4005 return c.ToString();
4023 public async Task<dynamic> FillIn(
string objectId,
string text,
int delayMs = 0,
string sessionId =
null)
4025 sessionId ??= GetEffectiveSessionId();
4028 bool success = await TryInsertText(objectId, text, sessionId).ConfigureAwait(
false);
4029 if (success)
return true;
4032 success = await TryJsDirectFill(objectId, text, sessionId).ConfigureAwait(
false);
4033 if (success)
return true;
4036 await FocusElement(objectId, sessionId).ConfigureAwait(
false);
4039 success = await TryTypeWithKeyEvents(objectId, text, delayMs, sessionId).ConfigureAwait(
false);
4040 if (success)
return true;
4044 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to fill element [{objectId}] with [{text}] using all methods.");
4058 private async Task<bool> TryTypeWithKeyEvents(
string objectId,
string text,
int delayMs,
string sessionId)
4062 await DispatchTextCharByChar(text, 0 < delayMs ? delayMs : 50, sessionId).ConfigureAwait(
false);
4065 string actualValue = await GetElementValue(objectId, sessionId).ConfigureAwait(
false);
4066 if (actualValue == text)
4069 await BlurElement(objectId, sessionId).ConfigureAwait(
false);
4073 catch (Exception ex)
4075 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to type text, trying next method.",
this, GPALObjectType.PuppeteerCommunicator, ex);
4088 private async Task<bool> TryInsertText(
string objectId,
string text,
string sessionId)
4093 await SendCommand(DevToolsMethods.InputInsertText,
new { text }, sessionId).ConfigureAwait(
false);
4096 string actualValue = await GetElementValue(objectId, sessionId).ConfigureAwait(
false);
4097 if (actualValue == text)
4099 await BlurElement(objectId, sessionId).ConfigureAwait(
false);
4103 catch (Exception ex)
4105 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to type text, trying next method.",
this, GPALObjectType.PuppeteerCommunicator, ex);
4119 private async Task<bool> TryJsDirectFill(
string objectId,
string text,
string sessionId)
4128 // Trigger everything frameworks listen to
4129 const desc = Object.getOwnPropertyDescriptor(this, 'value') ||
4130 Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value') ||
4131 Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value');
4133 // Bypass any framework value setters that block direct assignment
4134 if (desc && desc.set) {
4135 desc.set.call(this, value);
4138 this.dispatchEvent(new Event('input', { bubbles: true }));
4139 this.dispatchEvent(new Event('change', { bubbles: true }));
4142 await SendCommand(DevToolsMethods.RuntimeCallFunctionOn,
new
4145 functionDeclaration = js,
4146 arguments = new[] { new { value = text } },
4147 contextId = CurrentContextId,
4149 }, sessionId).ConfigureAwait(
false);
4152 string actualValue = await GetElementValue(objectId, sessionId).ConfigureAwait(
false);
4153 if (actualValue == text)
4155 await BlurElement(objectId, sessionId).ConfigureAwait(
false);
4159 catch (Exception ex)
4161 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to type text, trying next method.",
this, GPALObjectType.PuppeteerCommunicator, ex);
4173 private async Task<string> GetElementValue(
string objectId,
string sessionId)
4175 var result = await SendCommand<dynamic>(DevToolsMethods.RuntimeCallFunctionOn,
new
4178 functionDeclaration =
"function() { return this.value; }",
4179 contextId = CurrentContextId,
4180 returnByValue = true
4181 }, sessionId).ConfigureAwait(
false);
4184 return result?.result?.value?.ToString() ??
string.Empty;
4193 internal async Task FocusElement(
string objectId,
string sessionId)
4195 await SendCommand(DevToolsMethods.RuntimeCallFunctionOn,
new
4198 contextId = CurrentContextId,
4199 functionDeclaration =
"function() { this.focus(); }"
4200 }, sessionId).ConfigureAwait(
false);
4209 private async Task BlurElement(
string objectId,
string sessionId)
4211 await SendCommand(DevToolsMethods.RuntimeCallFunctionOn,
new
4214 contextId = CurrentContextId,
4215 functionDeclaration =
"function() { this.blur(); }"
4216 }, sessionId).ConfigureAwait(
false);
4231 public async Task FillInAppend(
string objectId,
string text,
int delayMs = 0,
string sessionId =
null)
4233 sessionId ??= GetEffectiveSessionId();
4235 var currentResp = await SendCommand<object>(DevToolsMethods.RuntimeCallFunctionOn,
new
4238 functionDeclaration =
"function() { return this.value || ''; }",
4239 contextId = CurrentContextId,
4240 returnByValue = true
4241 }, sessionId).ConfigureAwait(
false);
4243 string current = currentResp?.result?.value?.ToString() ??
"";
4244 await
FillIn(objectId, current + text, delayMs, sessionId).ConfigureAwait(
false);
4259 public async Task
FillInInsert(
string objectId,
string text,
int delayMs = 0,
string sessionId =
null)
4263 var currentResp = await SendCommand<object>(DevToolsMethods.RuntimeCallFunctionOn,
new
4266 functionDeclaration =
"function() { return this.value || ''; }",
4267 contextId = CurrentContextId,
4268 returnByValue = true
4269 }, sessionId).ConfigureAwait(
false);
4271 string current = currentResp?.result?.value?.ToString() ??
"";
4272 await
FillIn(objectId, text + current, delayMs, sessionId).ConfigureAwait(
false);
4287 public async Task
FillInOverwrite(
string objectId,
string text,
int delayMs = 0,
string sessionId =
null)
4289 await
FillIn(objectId, text, delayMs, sessionId).ConfigureAwait(
false);
4299 public async Task<dynamic> FireChangeEvent(
string objectId,
string sessionId =
null)
4301 sessionId ??= GetEffectiveSessionId();
4302 return await SendCommand<object>(DevToolsMethods.RuntimeCallFunctionOn,
new
4305 contextId = CurrentContextId,
4306 functionDeclaration =
"function() {{ this.dispatchEvent(new Event('change')); }}"
4307 }, sessionId).ConfigureAwait(
false);
4316 public async Task
Focus(
int backendNodeId,
string sessionId =
null)
4319 var nodeId = await GetNodeIdFromBackendNodeId(backendNodeId, sessionId).ConfigureAwait(
false);
4320 await SendCommand<object>(DevToolsMethods.DOMFocus,
new { nodeId }, sessionId).ConfigureAwait(
false);
4329 public async Task<dynamic> Forward(
string sessionId =
null)
4331 sessionId ??= GetEffectiveSessionId();
4332 dynamic history = await SendCommand<object>(DevToolsMethods.PageGetNavigationHistory,
new { }, sessionId).ConfigureAwait(
false);
4333 if (history?.currentIndex !=
null)
4335 var entries = (IEnumerable<dynamic>)history.entries;
4336 var nextEntry = entries.ElementAtOrDefault((
int)history.currentIndex + 1);
4337 if (nextEntry?.
id !=
null)
4339 var result = await SendCommand(DevToolsMethods.PageNavigateToHistoryEntry,
new { entryId = (int)nextEntry.id }, sessionId).ConfigureAwait(
false);
4343 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No next page in history",
this, GPALObjectType.PuppeteerClient);
4354 public async Task<bool> FullScreen(
string sessionId =
null)
4356 if (
true == Browser.BrowserSettings.UseHeadless)
4357 return await
Maximize().ConfigureAwait(
false);
4364 await SendCommand(DevToolsMethods.BrowserSetWindowBounds,
new
4367 bounds = new { windowState =
"fullscreen" }
4368 }, sessionId).ConfigureAwait(
false);
4382 public async Task<dynamic> GetAttribute(
int backendNodeId,
string attribute,
string sessionId =
null)
4384 var nodeId = await GetNodeIdFromBackendNodeId(backendNodeId, sessionId).ConfigureAwait(
false);
4385 sessionId ??= GetEffectiveSessionId();
4386 return await SendCommand<object>(DevToolsMethods.DOMGetAttributes,
new { nodeId, name = attribute }, sessionId).ConfigureAwait(
false);
4399 public async Task<Rectangle> GetBoundingClientRect(
int backendNodeId,
string sessionId =
null)
4401 sessionId ??= GetEffectiveSessionId();
4404 bool isInFrame = CurrentContextId.HasValue;
4405 string effectiveSessionId = isInFrame && !
string.IsNullOrEmpty(CurrentFrameSessionId)
4406 ? CurrentFrameSessionId
4410 if (CurrentContextId.HasValue && !
string.IsNullOrEmpty(CurrentFrameSessionId))
4415 var resolveResult = await SendCommand<dynamic>(DevToolsMethods.DOMResolveNode,
new
4417 backendNodeId = backendNodeId,
4418 executionContextId = CurrentContextId.Value
4419 }, CurrentFrameSessionId).ConfigureAwait(
false);
4421 string objectId = resolveResult?.@
object?.objectId;
4423 if (!
string.IsNullOrEmpty(objectId))
4426 var rectResult = await SendCommand<dynamic>(DevToolsMethods.RuntimeCallFunctionOn,
new
4428 objectId = objectId,
4429 functionDeclaration =
"function() { const r = this.getBoundingClientRect(); return {x: r.left, y: r.top, width: r.width, height: r.height}; }",
4430 contextId = CurrentContextId,
4431 returnByValue = true
4432 }, CurrentFrameSessionId).ConfigureAwait(
false);
4434 if (rectResult?.result?.value !=
null)
4436 var r = rectResult.result.value;
4437 if (0 < (
int)r.width && 0 < (
int)r.height)
4439 return new Rectangle((
int)r.x, (
int)r.y, (
int)r.width, (
int)r.height);
4447 Point pageOffset = Point.Empty;
4451 pageOffset =
new Point(await WindowPageOffsetX(effectiveSessionId).ConfigureAwait(
false),
4452 await WindowPageOffsetY(effectiveSessionId).ConfigureAwait(
false));
4462 var nodeId = await GetNodeIdFromBackendNodeId(backendNodeId, effectiveSessionId).ConfigureAwait(
false);
4463 dynamic box = await SendCommand<dynamic>(DevToolsMethods.DOMGetBoxModel,
new { nodeId }, effectiveSessionId).ConfigureAwait(
false);
4467 if (boxModel?.
Model !=
null)
4474 return new Rectangle(
4482 catch (Exception ex)
4485 $
"DOM.getBoxModel failed",
4486 this, GPALObjectType.PuppeteerCommunicator, ex);
4489 return new Rectangle(-1, -1, -1, -1);
4499 public async Task<dynamic> GetContentAndCss(
string elementId,
string sessionId =
null)
4501 sessionId ??= GetEffectiveSessionId();
4502 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
4504 expression = $
"let el = document.getElementById('{elementId}'); JSON.stringify({{content: el.innerHTML, css: getComputedStyle(el)}})",
4505 contextId = CurrentContextId,
4506 returnByValue = false
4507 }, sessionId).ConfigureAwait(
false);
4516 public async Task<Dictionary<string, object>> GetCssAttributes(
int backendNodeId,
string sessionId =
null)
4518 sessionId ??= GetEffectiveSessionId();
4522 var nodeId = await GetNodeIdFromBackendNodeId(backendNodeId, sessionId).ConfigureAwait(
false);
4524 await SendCommand<object>(DevToolsMethods.DOMEnable,
new { }, sessionId).ConfigureAwait(
false);
4525 await SendCommand<object>(DevToolsMethods.CSSEnable,
new { }, sessionId).ConfigureAwait(
false);
4527 var result = await SendCommand<dynamic>(DevToolsMethods.CSSGetComputedStyleForNode,
new { nodeId }, sessionId).ConfigureAwait(
false);
4529 if (result?.computedStyle ==
null)
4531 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"CSS.getComputedStyleForNode returned null for nodeId [{nodeId}]",
this, GPALObjectType.PuppeteerCommunicator);
4535 var relevantProps =
new HashSet<string>(
new[]
4537 "display",
"visibility",
"opacity",
"pointer-events",
"overflow",
"clip-path",
4538 "position",
"top",
"left",
"right",
"bottom",
"z-index",
4539 "width",
"height",
"min-width",
"min-height",
"max-width",
"max-height",
4540 "margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
4541 "padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
4542 "border",
"border-width",
"border-style",
"border-color",
"box-sizing",
4543 "flex",
"flex-direction",
"flex-wrap",
"flex-grow",
"flex-shrink",
"flex-basis",
4544 "justify-content",
"align-items",
"align-self",
"order",
4545 "grid",
"grid-template",
"grid-area",
"grid-column",
"grid-row",
4546 "transform",
"transform-origin",
"transition",
"animation",
4547 "cursor",
"user-select",
"content",
"filter"
4550 var emptyValues =
new HashSet<string> {
"",
"none",
"normal",
"inherit",
"initial",
"unset",
"auto",
"0px",
"0",
"transparent" };
4552 var computedStyles =
new Dictionary<string, object>();
4553 foreach (var style
in result.computedStyle)
4555 string name = style.name?.ToString();
4556 string value = style.value?.ToString()?.Trim();
4558 if (name !=
null && relevantProps.Contains(name) && !
string.IsNullOrEmpty(value) && !emptyValues.Contains(value))
4560 computedStyles[name] = value;
4564 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Retrieved [{computedStyles.Count}] filtered CSS attributes for nodeId [{nodeId}]",
this, GPALObjectType.PuppeteerCommunicator);
4565 return computedStyles;
4567 catch (Exception ex)
4569 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to get CSS attributes",
this, GPALObjectType.PuppeteerCommunicator, ex);
4574 await SendCommand<object>(DevToolsMethods.CSSDisable,
new { }, sessionId).ConfigureAwait(
false);
4575 await SendCommand<object>(DevToolsMethods.DOMDisable,
new { }, sessionId).ConfigureAwait(
false);
4629 public async Task<Dictionary<string, object>>
GetDomAttributes(
int backendNodeId,
string sessionId =
null)
4636 var nodeId = await GetNodeIdFromBackendNodeId(backendNodeId, sessionId).ConfigureAwait(
false);
4637 var result = await SendCommand<object>(DevToolsMethods.DOMGetAttributes,
new
4640 }, sessionId).ConfigureAwait(
false);
4642 if (result?.attributes ==
null)
4644 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"DOM.getAttributes returned null for nodeId [{nodeId}]",
this, GPALObjectType.PuppeteerCommunicator);
4649 var attributes =
new Dictionary<string, object>();
4650 for (
int i = 0; i < result.attributes.Count - 1; i += 2)
4652 attributes[result.attributes[i]?.ToString()] = result.attributes[i + 1]?.ToString();
4655 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Retrieved [{attributes.Count}] DOM attributes for nodeId [{nodeId}]",
this, GPALObjectType.PuppeteerCommunicator);
4658 catch (Exception ex)
4660 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to get DOM attributes for nodeId [{backendNodeId}]",
this, GPALObjectType.PuppeteerCommunicator, ex);
4671 public async Task<Dictionary<string, object>> GetDomProperties(
int backendNodeId,
string sessionId =
null)
4673 sessionId ??= GetEffectiveSessionId();
4679 var resolve = await SendCommand<dynamic>(DevToolsMethods.DOMResolveNode,
new { backendNodeId, pierce = true }, sessionId).ConfigureAwait(
false);
4680 string objectId = resolve.@
object.objectId;
4683 var jsProps = await SendCommand<dynamic>(DevToolsMethods.RuntimeCallFunctionOn,
new
4686 functionDeclaration =
@"
4689 if ('textContent' in this) props.textContent = this.textContent;
4690 if ('innerHTML' in this) props.innerHTML = this.innerHTML;
4691 if ('innerText' in this) props.innerText = this.innerText;
4692 if ('outerHTML' in this) props.outerHTML = this.outerHTML;
4693 if ('className' in this) props.className = this.className;
4694 if ('disabled' in this) props.disabled = this.disabled;
4697 contextId = CurrentContextId,
4698 returnByValue = true
4699 }, sessionId).ConfigureAwait(
false);
4702 await SendCommand<dynamic>(DevToolsMethods.RuntimeReleaseObject,
4704 contextId = CurrentContextId,
4706 }, sessionId).ConfigureAwait(
false);
4708 var properties =
new Dictionary<string, object>();
4709 if (jsProps?.result?.value !=
null)
4710 properties = ((Newtonsoft.Json.Linq.JObject)jsProps.result.value).ToObject<Dictionary<string, object>>();
4713 properties = properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value ??
"");
4715 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Retrieved [{properties.Count}] DOM properties for backendNodeId [{backendNodeId}]",
this, GPALObjectType.PuppeteerCommunicator);
4718 catch (Exception ex)
4720 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to get DOM properties for backendNodeId [{backendNodeId}]",
this, GPALObjectType.PuppeteerCommunicator, ex);
5055 public async Task<string>
GetCurrentUrl(
string sessionId =
null)
5060 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
5062 expression =
"window.location.href",
5063 contextId = CurrentContextId,
5064 returnByValue = true
5065 }, sessionId).ConfigureAwait(
false);
5068 return result?.result?.value;
5077 public async Task<int> GetCurrentWindow(
string sessionId =
null)
5079 sessionId ??= GetEffectiveSessionId();
5081 var result = await SendCommand<dynamic>(
5082 DevToolsMethods.BrowserGetWindowForTarget,
5085 ).ConfigureAwait(
false);
5087 return (
int)result.windowId;
5097 public async Task<System.Drawing.Rectangle> GetWindowRectangle(
string sessionId =
null)
5099 sessionId ??= GetEffectiveSessionId();
5101 int windowId = await GetCurrentWindow(sessionId).ConfigureAwait(
false);
5103 dynamic result = await SendCommand<dynamic>(
5104 DevToolsMethods.BrowserGetWindowBounds,
5107 ).ConfigureAwait(
false);
5109 dynamic bounds = result?.bounds;
5112 return new System.Drawing.Rectangle(
5113 (
int)(bounds?.left ?? 0),
5114 (
int)(bounds?.top ?? 0),
5115 (
int)(bounds?.width ?? 0),
5116 (
int)(bounds?.height ?? 0));
5126 public async Task<dynamic> GetElementAttributeHash(
string elementId,
string sessionId =
null)
5128 sessionId ??= GetEffectiveSessionId();
5129 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
5132 expression =
"JSON.stringify(document.getElementById('" + elementId +
"').attributes)",
5133 contextId = CurrentContextId,
5134 returnByValue = false
5135 }, sessionId).ConfigureAwait(
false);
5150 public async Task<string> GetPageSource(
string sessionId =
null)
5152 if (sessionId ==
null)
5154 sessionId = GetEffectiveSessionId();
5158 var docResult = await SendCommand(
5159 DevToolsMethods.DOMGetDocument,
5160 new Dictionary<string, object> { {
"depth", 0 } },
5161 sessionId).ConfigureAwait(
false);
5163 if (docResult ==
null)
5165 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"DOM.getDocument returned null",
5166 this, GPALObjectType.PuppeteerCommunicator);
5167 return string.Empty;
5171 JObject rootDoc = docResult as JObject;
5172 if (rootDoc ==
null)
5176 string jsonStr = Newtonsoft.Json.JsonConvert.SerializeObject(docResult);
5177 rootDoc = JObject.Parse(jsonStr);
5179 catch (Exception ex)
5181 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
5182 "Failed to convert DOM.getDocument response to JObject: [" + ex.Message +
"]",
5183 this, GPALObjectType.PuppeteerCommunicator);
5184 return string.Empty;
5189 JToken rootToken = rootDoc[
"root"] ?? rootDoc[
"result"]?[
"root"];
5191 if (rootToken ==
null || rootToken.Type != JTokenType.Object)
5193 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
5194 "No valid 'root' object found in DOM.getDocument response",
5195 this, GPALObjectType.PuppeteerCommunicator);
5196 return string.Empty;
5199 JObject root = (JObject)rootToken;
5201 if (!
long.TryParse(root[
"nodeId"]?.ToString(), out
long rootNodeId))
5203 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Invalid or missing nodeId in root",
5204 this, GPALObjectType.PuppeteerCommunicator);
5205 return string.Empty;
5209 var htmlResult = await SendCommand(
5210 DevToolsMethods.DOMGetOuterHTML,
5211 new Dictionary<string, object> { {
"nodeId", rootNodeId } },
5212 sessionId).ConfigureAwait(
false);
5214 if (htmlResult ==
null)
5216 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"DOM.getOuterHTML returned null",
5217 this, GPALObjectType.PuppeteerCommunicator);
5218 return string.Empty;
5222 JObject htmlDoc = htmlResult as JObject;
5223 if (htmlDoc ==
null)
5227 string jsonStr = Newtonsoft.Json.JsonConvert.SerializeObject(htmlResult);
5228 htmlDoc = JObject.Parse(jsonStr);
5230 catch (Exception ex)
5233 "Failed to convert DOM.getOuterHTML response to JObject: [" + ex.Message +
"]",
5234 this, GPALObjectType.PuppeteerCommunicator);
5235 return string.Empty;
5240 string fullSource = htmlDoc[
"outerHTML"]?.ToString();
5242 if (
string.IsNullOrWhiteSpace(fullSource))
5245 this, GPALObjectType.PuppeteerCommunicator);
5246 return string.Empty;
5263 public async Task<dynamic> GetParentNode(
string elementId,
string sessionId =
null)
5265 sessionId ??= GetEffectiveSessionId();
5266 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
5268 contextId = CurrentContextId,
5269 expression =
"document.getElementById('" + elementId +
"').parentNode"
5270 }, sessionId).ConfigureAwait(
false);
5281 public async Task<string> GetReadyStatus(
string sessionId,
string sessionToken)
5283 sessionId ??= GetEffectiveSessionId();
5284 string retVal =
null;
5286 if (lastSessionToken != sessionToken)
5288 lastErrorMessage.Clear();
5289 supressedMessage =
false;
5290 lastSessionToken = sessionToken;
5293 JObject result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
new
5295 expression =
"document.readyState",
5296 contextId = CurrentContextId,
5297 returnByValue = false
5298 }, sessionId).ConfigureAwait(
false);
5300 if (
null != result?[
"result"]?[
"value"])
5301 retVal = result?[
"result"]?[
"value"]?.ToString();
5303 retVal =
@"""loading""";
5305 string currentErrorMessage = $
"Document.Ready status [{retVal}]";
5307 if (
false == lastErrorMessage.Contains(currentErrorMessage))
5309 lastErrorMessage.Add(currentErrorMessage);
5311 supressedMessage =
false;
5313 else if (
false == supressedMessage)
5316 supressedMessage =
true;
5329 public async Task<dynamic> GetShadowRoot(
string css,
string sessionId =
null)
5331 sessionId ??= GetEffectiveSessionId();
5332 var nodeId = await QueryByCss(css, sessionId).ConfigureAwait(
false);
5333 return await SendCommand<object>(DevToolsMethods.DOMDescribeNode,
new { nodeId, pierce = true }, sessionId).ConfigureAwait(
false);
5359 public async Task<string> GetStorage(
5362 string domain =
null,
5363 string storeName =
null,
5367 sessionId ??= GetEffectiveSessionId();
5368 string resultJson =
"null";
5372 switch (storageType)
5377 dynamic cookiesResp = await SendCommand<dynamic>(DevToolsMethods.StorageGetCookies,
5379 sessionId).ConfigureAwait(
false);
5381 if (cookiesResp?.cookies !=
null)
5383 var filtered =
new List<dynamic>();
5386 foreach (dynamic c
in cookiesResp.cookies)
5388 if ((
false == UrlHelper.CookieDomainMatches(((dynamic)c).domain?.ToString(), domain)) ||
5389 (!
string.IsNullOrEmpty(path) && ((dynamic)c).path != path) ||
5390 (!
string.IsNullOrEmpty(key) && ((dynamic)c).name != key))
5395 if (((dynamic)c).partitionKey !=
null)
5399 Key = ((dynamic)c).name,
5400 Domain = ((dynamic)c).domain,
5401 Path = ((dynamic)c).path,
5402 Value = ((dynamic)c).value,
5403 Expires = ((dynamic)c).expires,
5405 Secure = ((dynamic)c).secure,
5406 HttpOnly = ((dynamic)c).httpOnly,
5407 SameSite = ((dynamic)c).sameSite,
5408 Session = ((dynamic)c).session,
5409 Priority = ((dynamic)c).priority,
5410 SameParty = ((dynamic)c).sameParty,
5411 SourceScheme = ((dynamic)c).sourceScheme,
5412 SourcePort = ((dynamic)c).sourcePort,
5414 PartitionKey = ((dynamic)c).partitionKey
5421 Key = ((dynamic)c).name,
5422 Domain = ((dynamic)c).domain,
5423 Path = ((dynamic)c).path,
5424 Value = ((dynamic)c).value,
5425 Expires = ((dynamic)c).expires,
5427 Secure = ((dynamic)c).secure,
5428 HttpOnly = ((dynamic)c).httpOnly,
5429 SameSite = ((dynamic)c).sameSite,
5430 Session = ((dynamic)c).session,
5431 Priority = ((dynamic)c).priority,
5432 SameParty = ((dynamic)c).sameParty,
5433 SourceScheme = ((dynamic)c).sourceScheme,
5434 SourcePort = ((dynamic)c).sourcePort
5439 resultJson = JsonConvert.SerializeObject(filtered);
5443 case "localStorage":
5444 case "sessionStorage":
5446 string js =
string.IsNullOrEmpty(key)
5449 for(let i=0;i<{storageType}.length;i++){{ let k={storageType}.key(i); obj[k]={storageType}.getItem(k); }}
5450 return JSON.stringify(obj);
5452 : $
"{storageType}.getItem('{EscapeJsString(key)}');";
5454 var evalResult = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
5457 awaitPromise = true,
5458 contextId = CurrentContextId,
5459 returnByValue = true
5460 }, sessionId).ConfigureAwait(
false);
5462 resultJson = evalResult?.result?.value?.ToString() ??
"null";
5469 if (
string.IsNullOrEmpty(path))
5474 let dbs = await indexedDB.databases().ConfigureAwait(false);
5475 return JSON.stringify(dbs.map(db => ({ name: db.name, version: db.version })));
5478 else if (
string.IsNullOrEmpty(storeName))
5483 let req = indexedDB.open('{EscapeJsString(path)}');
5484 return new Promise((resolve) => {{
5485 req.onsuccess = e => {{
5486 let db = e.target.result;
5487 let stores = Array.from(db.objectStoreNames);
5489 resolve(JSON.stringify(stores));
5491 req.onerror = () => resolve(null);
5495 else if (
string.IsNullOrEmpty(key))
5500 let req = indexedDB.open('{EscapeJsString(path)}');
5501 return new Promise((resolve) => {{
5502 req.onsuccess = e => {{
5503 let db = e.target.result;
5504 let tx = db.transaction('{EscapeJsString(storeName)}', 'readonly');
5505 let store = tx.objectStore('{EscapeJsString(storeName)}');
5507 let cursorReq = store.openKeyCursor();
5508 cursorReq.onsuccess = function(event) {{
5509 let cursor = event.target.result;
5511 keys.push(cursor.key);
5515 resolve(JSON.stringify(keys));
5518 cursorReq.onerror = () => {{ db.close(); resolve(null); }};
5520 req.onerror = () => resolve(null);
5529 let req = indexedDB.open('{EscapeJsString(path)}');
5530 return new Promise((resolve) => {{
5531 req.onsuccess = e => {{
5532 let db = e.target.result;
5533 let tx = db.transaction('{EscapeJsString(storeName)}', 'readonly');
5534 let store = tx.objectStore('{EscapeJsString(storeName)}');
5535 let getReq = store.get('{EscapeJsString(key)}');
5536 getReq.onsuccess = () => {{
5538 resolve(JSON.stringify(getReq.result));
5540 getReq.onerror = () => {{ db.close(); resolve(null); }};
5542 req.onerror = () => resolve(null);
5547 var evalDb = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
5550 awaitPromise = true,
5551 contextId = CurrentContextId,
5552 returnByValue = true
5553 }, sessionId).ConfigureAwait(
false);
5555 resultJson = evalDb?.result?.value?.ToString() ??
"null";
5563 if (
string.IsNullOrEmpty(storeName))
5567 js =
string.IsNullOrEmpty(key)
5568 ?
@"(async ()=>{let c=await caches.keys(); return JSON.stringify(c).ConfigureAwait(false);})()"
5569 :
$@"(async () => {{
5570 const response = await caches.match('{EscapeJsString(key)}').ConfigureAwait(false);
5572 const data = await response.json().ConfigureAwait(false);
5573 return JSON.stringify(data);
5578 else if (
string.IsNullOrEmpty(key))
5581 js =
$@"(async () => {{
5582 const cache = await caches.open('{EscapeJsString(storeName)}').ConfigureAwait(false);
5583 const keys = await cache.keys().ConfigureAwait(false);
5584 const keyUrls = keys.map(k => k.url); // keys are Request objects; we return clean URLs
5585 return JSON.stringify(keyUrls);
5591 js =
$@"(async () => {{
5592 const cache = await caches.open('{EscapeJsString(storeName)}').ConfigureAwait(false);
5593 const response = await cache.match('{EscapeJsString(key)}').ConfigureAwait(false);
5595 const contentType = response.headers.get('content-type') || '';
5597 if (contentType.includes('application/json') || contentType.includes('text/json')) {{{{
5598 data = await response.json().ConfigureAwait(false);
5600 data = await response.text().ConfigureAwait(false);
5602 return JSON.stringify(data);
5608 var evalCache = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
5611 awaitPromise = true,
5612 contextId = CurrentContextId,
5613 returnByValue = true
5614 }, sessionId).ConfigureAwait(
false);
5616 resultJson = evalCache?.result?.value?.ToString() ??
"null";
5621 GPAL.PublishSimpleEvent(
5622 GPALEventType.WARNING,
5623 $
"Nothing to do because StorageType is [{storageType}] d[{domain}] sn[{storeName}] p[{path}] k[{key}]",
5625 GPALObjectType.PuppeteerCommunicator);
5629 GPAL.PublishSimpleEvent(
5630 GPALEventType.ERROR,
5631 $
"Unsupported storage type: [{storageType}]",
5633 GPALObjectType.PuppeteerCommunicator);
5637 catch (Exception ex)
5639 GPAL.PublishSimpleEvent(
5640 GPALEventType.EXCEPTION,
5641 $
"GetStorage failed for [{storageType}] d[{domain}] sn[{storeName}] p[{path}] k[{key}]",
5643 GPALObjectType.PuppeteerCommunicator,
5655 public async Task<string> GetUserAgent(
string sessionId =
null)
5657 sessionId ??= GetEffectiveSessionId();
5658 JObject result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
5661 expression =
"navigator.userAgent",
5662 contextId = CurrentContextId,
5663 returnByValue = true
5664 }, sessionId).ConfigureAwait(
false);
5665 return result?[
"result"]?[
"value"]?.ToString();
5680 public async Task<bool>
GoTo(
string url,
string sessionId =
null)
5683 return await NavigateToUrl(url, sessionId).ConfigureAwait(
false);
5693 public async Task<string>
GoToTab(
object tabIdOrUrlOrIndex,
string sessionId =
null)
5695 string targetId =
null;
5696 var windowArray = _windowSessionsQueue.ToArray();
5698 if (tabIdOrUrlOrIndex is
int index)
5700 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
5702 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No active window for GoToTab",
this, GPALObjectType.PuppeteerCommunicator);
5706 var tabQueue = windowArray[_activeWindowIndex].TabQueue.ToArray();
5707 if (index < 0 || index >= tabQueue.Length)
5709 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Invalid tab index [{index}]",
this, GPALObjectType.PuppeteerCommunicator);
5714 targetId = tabQueue[index].Key;
5715 if (
string.IsNullOrEmpty(targetId))
5717 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No targetId at index [{index}]",
this, GPALObjectType.PuppeteerCommunicator);
5724 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId },
null).ConfigureAwait(
false);
5725 SetActiveTabIndex(index);
5730 else if (tabIdOrUrlOrIndex is
string str)
5732 targetId = await GetTargetTabIdByUrl(str, sessionId).ConfigureAwait(
false);
5733 if (
string.IsNullOrEmpty(targetId))
5736 foreach (var window
in windowArray)
5738 var tabArray = window.TabQueue.ToArray();
5739 if (tabArray.Any(kvp => kvp.Key == str))
5747 if (
string.IsNullOrEmpty(targetId))
5749 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No target found for tabId or URL [{str}]",
this, GPALObjectType.PuppeteerCommunicator);
5753 int windowIndex = GetCurrentWindowIndex(targetId);
5754 if (windowIndex < 0)
5756 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Tab with targetId [{targetId}] not found in any window",
this, GPALObjectType.PuppeteerCommunicator);
5760 SetActiveWindowIndex(windowIndex);
5761 var tabQueue = windowArray[windowIndex].TabQueue.ToArray();
5762 int tabIndex = Array.FindIndex(tabQueue, kvp => kvp.Key == targetId);
5765 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Tab with targetId [{targetId}] not found in window [{windowArray[windowIndex].BrowserContextId}]",
this, GPALObjectType.PuppeteerCommunicator);
5769 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId },
null).ConfigureAwait(
false);
5770 SetActiveTabIndex(tabIndex);
5772 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Switched to tab with targetId [{targetId}], windowIndex [{windowIndex}], tabIndex [{tabIndex}]",
this, GPALObjectType.PuppeteerCommunicator);
5776 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Invalid input for GoToTab [{tabIdOrUrlOrIndex}]",
this, GPALObjectType.PuppeteerCommunicator);
5787 public async Task<dynamic> HideElement(
int backendNodeId,
string sessionId =
null)
5789 return await SetAttribute(backendNodeId,
"style",
"display: none", sessionId).ConfigureAwait(
false);
5799 public async Task Hover(
int backendNodeId,
string sessionId =
null)
5801 sessionId ??= GetEffectiveSessionId();
5802 var x = await GetElementRandomCenterX(backendNodeId, sessionId).ConfigureAwait(
false);
5803 var y = await GetElementRandomCenterY(backendNodeId, sessionId).ConfigureAwait(
false);
5804 await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new { type =
"mouseMoved", button =
"none", x, y }, sessionId).ConfigureAwait(
false);
5819 public async Task<bool>
IsClickable(
string elementId,
string sessionId =
null)
5823 List<GPALElement> elems = await
EvaluateSelector(elementId, sessionId).ConfigureAwait(
false);
5824 List<dynamic> responses =
new List<dynamic>();
5826 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
5828 contextId = CurrentContextId,
5829 expression =
"document.getElementById('" + elementId +
"').checkVisibility()"
5830 }, sessionId).ConfigureAwait(
false);
5831 return (
bool)result.value;
5846 public async Task<bool> IsDisplayed(
string elementId,
string sessionId =
null)
5848 sessionId ??= GetEffectiveSessionId();
5849 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
5851 contextId = CurrentContextId,
5852 expression =
"document.getElementById('" + elementId +
"').offsetParent !== null"
5853 }, sessionId).ConfigureAwait(
false);
5854 return (
bool)result.value;
5869 public async Task<bool> IsEnabled(
string elementId,
string sessionId =
null)
5871 sessionId ??= GetEffectiveSessionId();
5872 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
5874 contextId = CurrentContextId,
5875 expression =
"!document.getElementById('" + elementId +
"').disabled"
5876 }, sessionId).ConfigureAwait(
false);
5877 return (
bool)result.value;
5887 public async Task<bool> IsEndOfPage(
string sessionId =
null)
5889 sessionId ??= GetEffectiveSessionId();
5893 var result = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
5898 const overflow = getComputedStyle(document.documentElement).overflow;
5899 if (overflow !== 'hidden') {
5900 return (window.innerHeight + window.scrollY) >= document.body.scrollHeight;
5902 const scrollable = Array.from(document.querySelectorAll('*')).find(
5903 el => el.scrollHeight > el.clientHeight && getComputedStyle(el).overflowY !== 'hidden'
5905 return (scrollable.clientHeight + scrollable.scrollTop) > scrollable.scrollHeight;
5910 contextId = CurrentContextId,
5911 returnByValue = true
5912 }, sessionId).ConfigureAwait(
false);
5915 return (
bool)result?[
"result"]?[
"value"];
5917 catch (Exception ex)
5919 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Error detecting end of page, returning [true]",
this, GPALObjectType.PuppeteerCommunicator, ex);
5942 public async Task<bool> ElementFromPoint(
GPALElement element,
int x,
int y,
string sessionId =
null)
5944 sessionId ??= GetEffectiveSessionId();
5949 var resolved = await SendCommand<dynamic>(DevToolsMethods.DOMResolveNode,
new
5951 backendNodeId = element.ElementBackendNodeId
5952 }, sessionId).ConfigureAwait(
false);
5954 string objectId = resolved?.@
object?.objectId;
5956 if (
true ==
string.IsNullOrEmpty(objectId))
5963 var result = await SendCommand<dynamic>(DevToolsMethods.RuntimeCallFunctionOn,
new
5965 objectId = objectId,
5966 functionDeclaration =
@"function (x, y) {
5967 const hit = document.elementFromPoint(x, y);
5968 if (hit === this || this.contains(hit)) { return ''; }
5969 if (!hit) { return 'nothing'; }
5971 + (hit.id ? '#' + hit.id : '')
5972 + (hit.className ? '.' + String(hit.className).trim().split(/\s+/).join('.') : '');
5974 arguments = new object[] { new { value = x }, new { value = y } },
5975 returnByValue =
true
5976 }, sessionId).ConfigureAwait(
false);
5978 string covering = result?.result?.value as string;
5980 if (
false ==
string.IsNullOrEmpty(covering))
5981 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"[{covering}] is what is at viewport [{x}, {y}], not [{element.TagName}]",
null, GPALObjectType.Browser);
5983 return true ==
string.IsNullOrEmpty(covering) &&
null != result?.result?.value;
5986 public async Task<bool> IsVisibleInViewport(dynamic elemsOrBackendNodeId,
string sessionId =
null)
5988 sessionId ??= GetEffectiveSessionId();
5989 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
new
5992 height: window.innerHeight || document.documentElement.clientHeight,
5993 width: window.innerWidth || document.documentElement.clientWidth
5995 contextId = CurrentContextId,
5996 returnByValue = true
5997 }, sessionId).ConfigureAwait(
false);
5999 int viewportHeight = 0;
6000 int viewportWidth = 0;
6001 bool isInViewport =
true;
6002 if (result?.result?.value !=
null)
6004 var dict = ((Newtonsoft.Json.Linq.JObject)result.result.value).ToObject<Dictionary<string, int>>();
6005 viewportHeight = dict[
"height"];
6006 viewportWidth = dict[
"width"];
6009 if (elemsOrBackendNodeId is List<GPALElement>)
6010 foreach (GPALElement element
in elemsOrBackendNodeId)
6012 Rectangle rect = await GetBoundingClientRect(element.ElementBackendNodeId, sessionId).ConfigureAwait(
false);
6014 isInViewport =
true;
6016 isInViewport &= rect.Top >= 0 &&
6018 rect.Bottom <= viewportHeight &&
6019 rect.Right <= viewportWidth;
6023 Rectangle rect = await GetBoundingClientRect(elemsOrBackendNodeId, sessionId).ConfigureAwait(
false);
6025 isInViewport =
true;
6027 isInViewport &= rect.Top >= 0 &&
6029 rect.Bottom <= viewportHeight &&
6030 rect.Right <= viewportWidth;
6032 return isInViewport;
6045 public async Task ClickElement(GPALElement element,
string sessionId, List<dynamic> responses,
6046 ClickType clickType = ClickType.LeftClick,
6049 bool cdpSuccess =
false;
6050 bool forceJavscript =
false;
6051 bool jsSuccess =
false;
6052 bool isInFrame = CurrentContextId.HasValue;
6053 string effectiveSessionId = isInFrame && !
string.IsNullOrEmpty(CurrentFrameSessionId)
6054 ? CurrentFrameSessionId
6062 if (
true == Browser.BrowserSettings.UseHeadless)
6064 await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
new
6066 expression =
"document.body.offsetHeight;",
6067 contextId = CurrentContextId,
6068 returnByValue = true
6069 }, sessionId).ConfigureAwait(
false);
6071 await Task.Delay(80).ConfigureAwait(
false);
6074 var boxModel = await SendCommand<object>(DevToolsMethods.DOMGetBoxModel,
new
6076 objectId = element.ElementHandle
6077 }, effectiveSessionId).ConfigureAwait(
false);
6079 if (
null != boxModel)
6081 var contentQuad = boxModel?.model?.content;
6082 if (contentQuad ==
null || contentQuad.Count < 8)
6084 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"[{element.TagName}][{clickType}] failed: Unable to get coordinates (boxmodel)");
6088 float x = (float)contentQuad[0];
6089 float y = (float)contentQuad[1];
6090 float width = (float)(contentQuad[2] - contentQuad[0]);
6091 float height = (float)(contentQuad[7] - contentQuad[1]);
6093 var random =
new Random();
6095 var clickAreaWidth = width * (1f - 2 * margin);
6096 var clickAreaHeight = height * (1f - 2 * margin);
6098 x = x + width * margin + (float)random.NextDouble() * clickAreaWidth;
6099 y = y + height * margin + (float)random.NextDouble() * clickAreaHeight;
6104 if (
"A".Equals(element.TagName, StringComparison.OrdinalIgnoreCase))
6106 x = (float)contentQuad[0] + width / 2f;
6107 y = (float)contentQuad[1] + height / 2f;
6111 string button = clickType
switch
6113 ClickType.LeftClick =>
"left",
6114 ClickType.MiddleClick =>
"middle",
6115 ClickType.RightClick =>
"right",
6116 ClickType.LeftDoubleClick =>
"left",
6120 int clickCount = clickType == ClickType.LeftDoubleClick ? 2 : 1;
6123 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6125 type =
"mousePressed",
6131 timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
6132 }, effectiveSessionId).ConfigureAwait(
false));
6134 await Task.Delay(random.Next(60, 140)).ConfigureAwait(
false);
6137 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6139 type =
"mouseReleased",
6142 x = x + random.Next(-2, 3),
6143 y = y + random.Next(-2, 3),
6145 timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
6146 }, effectiveSessionId).ConfigureAwait(
false));
6153 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"CDP [{clickType}] has no on-screen point for [{element.TagName}][{element.Css}] at [{x},{y}]. Falling back to JavaScript.",
this, GPALObjectType.PuppeteerClient);
6154 forceJavscript =
true;
6160 float x = (float)element.BoundingRect.X;
6161 float y = (float)element.BoundingRect.Y;
6162 float width = (float)element.BoundingRect.Width;
6163 float height = (float)element.BoundingRect.Height;
6165 var random =
new Random();
6167 var clickAreaWidth = width * (1f - 2 * margin);
6168 var clickAreaHeight = height * (1f - 2 * margin);
6170 x = x + width * margin + (float)random.NextDouble() * clickAreaWidth;
6171 y = y + height * margin + (float)random.NextDouble() * clickAreaHeight;
6176 if (
"A".Equals(element.TagName, StringComparison.OrdinalIgnoreCase))
6178 x = (float)element.BoundingRect.X + width / 2f;
6179 y = (float)element.BoundingRect.Y + height / 2f;
6183 string button = clickType
switch
6185 ClickType.LeftClick =>
"left",
6186 ClickType.MiddleClick =>
"middle",
6187 ClickType.RightClick =>
"right",
6188 ClickType.LeftDoubleClick =>
"left",
6192 int clickCount = clickType == ClickType.LeftDoubleClick ? 2 : 1;
6195 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6197 type =
"mousePressed",
6203 timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
6204 }, effectiveSessionId).ConfigureAwait(
false));
6206 await Task.Delay(random.Next(60, 140)).ConfigureAwait(
false);
6209 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6211 type =
"mouseReleased",
6214 x = x + random.Next(-2, 3),
6215 y = y + random.Next(-2, 3),
6217 timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
6218 }, effectiveSessionId).ConfigureAwait(
false));
6223 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"CDP [{clickType}] failed. Falling back to JavaScript.",
this, GPALObjectType.PuppeteerClient);
6224 forceJavscript =
true;
6228 catch (Exception ex)
6230 if (!GPAL.NoFallbackRecoveryActions)
6231 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"CDP [{clickType}] failed.{(false == GPAL.NoFallbackRecoveryActions ? " Falling back to JavaScript.
" : " Javascript fallback disallowed.
")}",
this, GPALObjectType.PuppeteerClient, ex);
6235 if ((
false == cdpSuccess &&
false == GPAL.NoFallbackRecoveryActions) ||
true == forceJavscript)
6237 string selector = !
string.IsNullOrEmpty(element.Css) ? element.Css : element.Xpath;
6239 if (
string.IsNullOrEmpty(selector))
6240 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Fallback JavaScript failed: No valid XPath or CSS selector for [{element.TagName}]",
this, GPALObjectType.PuppeteerClient);
6242 var clickScript =
$@"
6246 const sel = `{selector.Replace("'", "\\'")}`;
6248 // XPath first -> CSS fallback
6249 const xp = document.evaluate(sel, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
6250 el = xp.singleNodeValue || document.querySelector(sel);
6251 if (!el) return false;
6253 // getBoundingClientRect is already viewport relative, which is what clientX and
6254 // clientY are defined as. adding the scroll offset turns them into document
6255 // coordinates and any handler that hit tests the event reads the wrong point
6256 const rect = el.getBoundingClientRect();
6257 const cx = rect.left + rect.width / 2;
6258 const cy = rect.top + rect.height / 2;
6260 const opts = {{ bubbles: true, cancelable: true, clientX: cx, clientY: cy, view: window }};
6262 // === LEFT CLICK (or double) ===
6263 if ({(clickType == ClickType.LeftClick || clickType == ClickType.LeftDoubleClick).ToString().ToLower()}) {{
6264 const count = {(clickType == ClickType.LeftDoubleClick ? 2 : 1)};
6265 for (let i = 0; i < count; i++) {{
6266 el.dispatchEvent(new MouseEvent('mousedown', {{...opts, button: 0, buttons: 1}}));
6267 el.dispatchEvent(new MouseEvent('mouseup', {{...opts, button: 0, buttons: 0}}));
6268 el.dispatchEvent(new MouseEvent('click', {{...opts, button: 0, detail: i+1}}));
6269 if (i === 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 80); // ~80ms between clicks
6274 // === MIDDLE CLICK (eBay pop-under) ===
6275 if ({(clickType == ClickType.MiddleClick).ToString().ToLower()}) {{
6276 el.dispatchEvent(new MouseEvent('mousedown', {{...opts, button: 1, buttons: 4}}));
6277 el.dispatchEvent(new MouseEvent('mouseup', {{...opts, button: 1, buttons: 0}}));
6278 el.dispatchEvent(new MouseEvent('click', {{...opts, button: 1}}));
6282 // === RIGHT CLICK (context menu) ===
6283 if ({(clickType == ClickType.RightClick).ToString().ToLower()}) {{
6284 el.dispatchEvent(new MouseEvent('mousedown', {{...opts, button: 2, buttons: 2}}));
6285 el.dispatchEvent(new MouseEvent('mouseup', {{...opts, button: 2, buttons: 0}}));
6286 el.dispatchEvent(new MouseEvent('contextmenu', {{...opts, button: 2}}));
6297 if (
false ==
string.IsNullOrEmpty(selector))
6299 dynamic jsResult = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
new
6301 expression = clickScript,
6302 objectGroup =
"node",
6303 contextId = CurrentContextId,
6304 returnByValue = true
6305 }, sessionId).ConfigureAwait(
false);
6307 responses.Add(jsResult);
6310 if (jsResult?.result?.value is
bool clicked && clicked)
6317 string clickPath =
true == cdpSuccess ?
"CDP" :
true == jsSuccess ?
"JavaScript" :
"nothing";
6319 GPAL.
PublishSimpleEvent(
true == cdpSuccess ||
true == jsSuccess ? GPALEventType.INFO : GPALEventType.WARNING,
6320 $
"Puppeteer [{clickType}] [{element.TagName}][{Browser.CurrentUOW.CurrentSelector.Name}] w/modifiers [{((PuppeteerClient)PuppeteerClient).GetModifiersString(modifiers)}] dispatched by [{clickPath}]",
6321 this, GPALObjectType.PuppeteerCommunicator);
6333 public async Task
DragAndDrop(
GPALElement element,
string sessionId, List<dynamic> responses,
int deltaX,
int deltaY,
int offsetX = 0,
int offsetY = 0)
6335 var startX = (float)element.
BoundingRect.
X + (
float)element.BoundingRect.Width / 2 + offsetX;
6336 var startY = (float)element.
BoundingRect.
Y + (
float)element.BoundingRect.Height / 2 + offsetY;
6337 var endX = startX + deltaX;
6338 var endY = startY + deltaY;
6340 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6342 type =
"mousePressed",
6348 timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
6349 }, sessionId).ConfigureAwait(
false));
6351 const int STEPS = 10;
6352 for (
int i = 1; i <= STEPS; i++)
6354 var x = startX + (endX - startX) * i / STEPS;
6355 var y = startY + (endY - startY) * i / STEPS;
6357 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6359 type =
"mouseMoved",
6365 timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
6366 }, sessionId).ConfigureAwait(
false));
6368 await Task.Delay(15).ConfigureAwait(
false);
6371 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6373 type =
"mouseReleased",
6379 timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
6380 }, sessionId).ConfigureAwait(
false));
6395 public async Task LeftClick(
string elementId,
string sessionId =
null)
6397 sessionId ??= GetEffectiveSessionId();
6398 List<GPALElement> elems = await EvaluateSelector(elementId, sessionId).ConfigureAwait(
false);
6399 List<dynamic> responses =
new List<dynamic>();
6403 await ScrollIntoView(elem, sessionId).ConfigureAwait(
false);
6404 await ClickElement(elem, sessionId, responses, ClickType.LeftClick).ConfigureAwait(
false);
6419 public async Task SetDownloadBehavior(
string downloadPath,
string sessionId =
null)
6421 sessionId ??= GetEffectiveSessionId();
6423 await SendCommand<object>(DevToolsMethods.BrowserSetDownloadBehavior,
new { behavior =
"allow", downloadPath = downloadPath }, sessionId).ConfigureAwait(
false);
6438 await SendCommand<object>(DevToolsMethods.BrowserSetDownloadBehavior,
new { behavior =
"default" }, sessionId).ConfigureAwait(
false);
6455 public async Task
LeftClickAndDownload(List<GPALElement> elems,
string downloadPath,
int modifiers,
string sessionId, List<dynamic> responses)
6459 string saveDirectory = Path.GetDirectoryName(downloadPath);
6467 await
ClickElement(elem, sessionId, responses, ClickType.LeftClick, modifiers).ConfigureAwait(
false);
6473 private static readonly Dictionary<string, List<string>> _attachedFilesPerSession =
new Dictionary<string, List<string>>();
6514 List<GPALElement> elems,
6518 List<dynamic> responses)
6523 var newPaths =
new List<string>();
6525 if (uploadFiles is
string singlePath)
6527 if (
"" != singlePath?.Trim())
6529 newPaths.Add(Path.GetFullPath(singlePath));
6532 else if (uploadFiles is
GPALFile pathEnumerable)
6534 foreach (var p
in pathEnumerable.Filenames)
6536 if (
"" != p?.Trim())
6538 newPaths.Add(Path.GetFullPath(p));
6548 if (0 == newPaths.Count)
6555 string nodeKey = $
"{sessionId}:{fileInput.ElementBackendNodeId}";
6558 string fileCountStr = fileInput.
GetAttribute(
"length");
6559 int browserFileCount = 0;
6561 if (
"" != fileCountStr)
6563 Int32.TryParse(fileCountStr, out browserFileCount);
6567 if (
false == _attachedFilesPerSession.TryGetValue(nodeKey, out var currentFiles))
6569 currentFiles =
new List<string>();
6570 _attachedFilesPerSession[nodeKey] = currentFiles;
6574 if (browserFileCount != currentFiles.Count)
6576 currentFiles.Clear();
6577 _attachedFilesPerSession[nodeKey] = currentFiles;
6581 currentFiles.AddRange(newPaths);
6583 var setFilesParams =
new
6585 nodeId = fileInput.ElementNodeId,
6586 files = currentFiles.ToArray()
6589 await SendCommand<object>(DevToolsMethods.DOMSetFileInputFiles, setFilesParams, sessionId).ConfigureAwait(
false);
6590 await FireChangeEvent(fileInput.ElementHandle, sessionId).ConfigureAwait(
false);
6594 status =
"upload_attached",
6595 pathsAdded = newPaths,
6596 totalFilesTracked = currentFiles.Count,
6597 browserReportedCount = browserFileCount
6611 public async Task LeftDoubleClick(
string elementId,
string sessionId =
null)
6613 sessionId ??= GetEffectiveSessionId();
6614 List<GPALElement> elems = await EvaluateSelector(elementId, sessionId).ConfigureAwait(
false);
6615 List<dynamic> responses =
new List<dynamic>();
6617 foreach (GPALElement elem
in elems)
6619 await ScrollIntoView(elem, sessionId).ConfigureAwait(
false);
6620 await ClickElement(elem, sessionId, responses, ClickType.LeftDoubleClick).ConfigureAwait(
false);
6632 public async Task<bool>
Maximize(
string sessionId =
null)
6639 await SendCommand(DevToolsMethods.BrowserSetWindowBounds,
new
6642 bounds = new { windowState =
"maximized" }
6643 }, sessionId).ConfigureAwait(
false);
6655 public async Task<bool>
Minimize(
string sessionId =
null)
6660 await SendCommand(DevToolsMethods.BrowserSetWindowBounds,
new
6662 windowId = windowId,
6663 bounds = new { windowState =
"minimized" }
6664 }, sessionId).ConfigureAwait(
false);
6677 public async Task<bool> MoveTo(List<GPALElement> elems,
string sessionId, List<dynamic> responses)
6685 await ScrollIntoView(element, sessionId).ConfigureAwait(
false);
6688 var random =
new Random();
6692 var x = (float)element.
BoundingRect.
X + (
float)element.BoundingRect.Width * margin + (float)random.NextDouble() * clickAreaWidth;
6693 var y = (float)element.
BoundingRect.
Y + (
float)element.BoundingRect.Height * margin + (float)random.NextDouble() * clickAreaHeight;
6695 if (
true ==
"A".Equals(element.
TagName))
6697 x = (float)element.
BoundingRect.
X + (
float)element.BoundingRect.Width / 2;
6698 y = (float)element.
BoundingRect.
Y + (
float)element.BoundingRect.Height / 2;
6701 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6703 type =
"mouseMoved",
6707 timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
6708 }, sessionId).ConfigureAwait(
false));
6710 catch (Exception ex)
6712 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Error moving to element [{element.ElementHandle}][{element.Css}]",
this, GPALObjectType.Puppeteer, ex);
6726 public async Task MoveTo(
int backendNodeId,
string sessionId =
null)
6728 sessionId ??= GetEffectiveSessionId();
6729 var x = await GetElementRandomCenterX(backendNodeId, sessionId).ConfigureAwait(
false);
6730 var y = await GetElementRandomCenterY(backendNodeId, sessionId).ConfigureAwait(
false);
6731 await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new { type =
"mouseMoved", x, y }, sessionId).ConfigureAwait(
false);
6747 public async Task ClickElement(
int x,
int y,
string sessionId, List<dynamic> responses,
6748 ClickType clickType = ClickType.LeftClick,
6751 sessionId ??= GetEffectiveSessionId();
6753 string button = clickType
switch
6755 ClickType.LeftClick =>
"left",
6756 ClickType.MiddleClick =>
"middle",
6757 ClickType.RightClick =>
"right",
6758 ClickType.LeftDoubleClick =>
"left",
6762 int clickCount = ClickType.LeftDoubleClick == clickType ? 2 : 1;
6763 var random =
new Random();
6766 await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6768 type =
"mouseMoved",
6772 }, sessionId).ConfigureAwait(
false);
6774 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6776 type =
"mousePressed",
6782 timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
6783 }, sessionId).ConfigureAwait(
false));
6785 await Task.Delay(random.Next(60, 140)).ConfigureAwait(
false);
6787 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6789 type =
"mouseReleased",
6795 timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
6796 }, sessionId).ConfigureAwait(
false));
6809 public async Task MoveTo(
int x,
int y,
string sessionId =
null)
6811 sessionId ??= GetEffectiveSessionId();
6812 await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new { type =
"mouseMoved", button =
"none", x, y }, sessionId).ConfigureAwait(
false);
6830 public async Task<string> NewTab(GPALUrl url =
null,
string sessionId =
null)
6833 url =
new GPALUrl();
6835 sessionId ??= GetEffectiveSessionId();
6836 url.ForUrl(MagicHelper.GetFullUrl(url.Url, Browser, out Browser._areRobotsAllowed));
6838 if (
false == Browser.AreRobotsAllowed &&
true == Browser.BrowserSettings.ObeyRobotsTxt)
6840 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Visiting [{url.Url}] is disallowed by robots.txt and your request to honor it. Not going to URL. Workflow will fail.",
this, GPALObjectType.PuppeteerCommunicator);
6841 url.ForUrl(
"https://google.com");
6844 var browserContextId = GetActiveWindowId();
6845 if (
string.IsNullOrEmpty(browserContextId))
6848 "NewTab: No active window found to attach tab",
6849 this, GPALObjectType.PuppeteerCommunicator);
6854 var result = await SendCommand<object>(DevToolsMethods.TargetCreateTarget,
6855 new { url = url.Url, browserContextId, background = true },
null).ConfigureAwait(
false);
6857 string targetId = result.targetId.ToString();
6860 await HandleNewTarget(targetId, browserContextId).ConfigureAwait(
false);
6863 $
"Opened new tab TargetId [{targetId}] in Window [{browserContextId}]",
6864 this, GPALObjectType.PuppeteerCommunicator);
6878 internal int GetCurrentTabIndex(
string currentTargetId)
6880 var windowArray = _windowSessionsQueue.ToArray();
6881 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
6883 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"No active window for NextTab",
this, GPALObjectType.PuppeteerCommunicator);
6887 var tabQueue = windowArray[_activeWindowIndex].TabQueue;
6888 var queueArray = tabQueue.ToArray();
6889 if (queueArray.Length == 0)
6891 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"No tabs for NextTab",
this, GPALObjectType.PuppeteerCommunicator);
6895 var currentIndex = Array.FindIndex(queueArray, kvp => kvp.Key == currentTargetId);
6896 if (currentIndex == -1)
6897 currentIndex = windowArray[_activeWindowIndex].ActiveTabIndex;
6899 return currentIndex;
6909 public async Task<string> NextTab(
string currentTargetId)
6911 var windowArray = _windowSessionsQueue.ToArray();
6912 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
6914 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No active window for NextTab",
this, GPALObjectType.PuppeteerCommunicator);
6918 var session = windowArray[_activeWindowIndex];
6919 var tabQueue = session.TabQueue;
6920 var browserContextId = session.BrowserContextId;
6923 var targetsResponse = await SendCommand<dynamic>(
6924 DevToolsMethods.TargetGetTargets,
6927 ).ConfigureAwait(
false);
6929 if (targetsResponse?.targetInfos ==
null)
6931 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"Failed to get targets list in NextTab sync",
this, GPALObjectType.PuppeteerCommunicator);
6936 var knownTargetIds =
new HashSet<string>(tabQueue.Select(kv => kv.Key));
6938 foreach (var targetInfo
in targetsResponse.targetInfos)
6940 string targetId = targetInfo.targetId?.ToString();
6941 string type = targetInfo.type?.ToString();
6942 string contextId = targetInfo.browserContextId?.ToString();
6944 if (
string.IsNullOrEmpty(targetId) || type !=
"page" || contextId != browserContextId)
6947 if (!knownTargetIds.Contains(targetId))
6950 await AddTabToQueue(targetId,
null,
false,
true).ConfigureAwait(
false);
6951 knownTargetIds.Add(targetId);
6956 var queueArray = tabQueue.ToArray();
6957 if (queueArray.Length == 0)
6959 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No tabs for NextTab",
this, GPALObjectType.PuppeteerCommunicator);
6966 var currentIndex = Array.FindIndex(queueArray, kvp => kvp.Key == currentTargetId);
6967 if (currentIndex == -1)
6969 currentIndex = session.ActiveTabIndex;
6972 var nextIndex = (currentIndex + 1) % queueArray.Length;
6973 var nextTargetId = await GoToTab(nextIndex).ConfigureAwait(
false);
6975 session.ActiveTabIndex = nextIndex;
6977 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
6978 $
"Switched to index [{nextIndex}], ID [{nextTargetId}], Active window tab queue size [{queueArray.Length}]",
6979 this, GPALObjectType.PuppeteerCommunicator);
6981 return nextTargetId;
7019 public async Task<string>
NextWindow(
string currentTargetId)
7021 var windowArray = _windowSessionsQueue.ToArray();
7022 if (windowArray.Length == 0)
7024 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No windows for NextWindow",
this, GPALObjectType.PuppeteerCommunicator);
7028 var currentWindowIndex = -1;
7029 for (
int i = 0; i < windowArray.Length; i++)
7031 var tabArray = windowArray[i].TabQueue.ToArray();
7032 if (Array.Exists(tabArray, kvp => kvp.Key == currentTargetId))
7034 currentWindowIndex = i;
7038 if (currentWindowIndex == -1)
7040 currentWindowIndex = _activeWindowIndex;
7043 var nextWindowIndex = (currentWindowIndex + 1) % windowArray.Length;
7044 SetActiveWindowIndex(nextWindowIndex);
7046 var nextWindowContextId = windowArray[nextWindowIndex].BrowserContextId;
7047 var nextTabQueue = windowArray[nextWindowIndex].TabQueue;
7048 var nextTabArray = nextTabQueue.ToArray();
7049 if (nextTabArray.Length == 0)
7051 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"No tabs in next window for NextWindow",
this, GPALObjectType.PuppeteerCommunicator);
7055 var nextTargetId = nextTabArray[windowArray[nextWindowIndex].ActiveTabIndex].Key;
7056 var nextSessionId = nextTabArray[windowArray[nextWindowIndex].ActiveTabIndex].Value;
7058 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId = nextTargetId },
null).ConfigureAwait(
false);
7060 if (await SendCommand<object>(DevToolsMethods.PageEnable,
new { }, nextSessionId).ConfigureAwait(
false) ==
null)
7062 var newSession = await SendCommand<object>(DevToolsMethods.TargetAttachToTarget,
new { targetId = nextTargetId, flatten = true },
null).ConfigureAwait(
false);
7063 if (newSession?.sessionId !=
null)
7065 nextSessionId = (string)newSession.sessionId;
7066 RemoveTabFromQueue(nextTargetId);
7067 await AddTabToQueue(nextTargetId, nextSessionId).ConfigureAwait(
false);
7071 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to re-attach NextWindow tab ID [{nextTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
7074 await SendCommand<object>(DevToolsMethods.PageDisable,
new { }, nextSessionId).ConfigureAwait(
false);
7078 await SendCommand<object>(DevToolsMethods.PageDisable,
new { }, nextSessionId).ConfigureAwait(
false);
7081 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"NextWindow: Switched to window index [{nextWindowIndex}], Context [{nextWindowContextId}], Tab ID [{nextTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
7082 return nextTargetId;
7091 public async Task<bool>
Normal(
string sessionId =
null)
7096 await SendCommand(DevToolsMethods.BrowserSetWindowBounds,
new
7098 windowId = windowId,
7099 bounds = new { windowState =
"normal" }
7100 }, sessionId).ConfigureAwait(
false);
7114 public async Task<string> OpenWindow(
string url =
"https://google.com",
string sessionId =
null)
7116 sessionId ??= GetEffectiveSessionId();
7119 var context = await SendCommand<object>(DevToolsMethods.TargetCreateBrowserContext,
new { }, sessionId).ConfigureAwait(
false);
7120 string browserContextId = context.browserContextId.ToString();
7122 var result = await SendCommand<object>(DevToolsMethods.TargetCreateTarget,
7123 new { url, browserContextId },
null).ConfigureAwait(
false);
7125 string targetId = result.targetId.ToString();
7128 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId },
null).ConfigureAwait(
false);
7129 var session = await SendCommand<object>(DevToolsMethods.TargetAttachToTarget,
7130 new { targetId, flatten = true },
null).ConfigureAwait(
false);
7132 string newSessionId = session.sessionId.ToString();
7135 var tabQueue =
new ConcurrentQueue<KeyValuePair<string, string>>();
7136 tabQueue.Enqueue(
new KeyValuePair<string, string>(targetId, newSessionId));
7139 _windowSessionsQueue.Enqueue(
new WindowSession(browserContextId, tabQueue, 0));
7142 SetActiveWindowIndex(_windowSessionsQueue.Count - 1);
7144 ((PuppeteerClient)PuppeteerClient)._currentTargetId = targetId;
7147 $
"Opened new window ContextId [{browserContextId}], TargetId [{targetId}], SessionId [{newSessionId}]",
7148 this, GPALObjectType.PuppeteerCommunicator);
7160 public async Task OverrideReferrer(
string url,
string sessionId =
null)
7162 sessionId ??= GetEffectiveSessionId();
7163 await SendCommand<object>(DevToolsMethods.NetworkSetExtraHTTPHeaders,
new { headers = new { Referer = url } }, sessionId).ConfigureAwait(
false);
7174 public async Task<dynamic> SetUserAgent(
string userAgent,
string sessionId =
null)
7176 sessionId ??= GetEffectiveSessionId();
7177 return await SendCommand<object>(DevToolsMethods.NetworkSetUserAgentOverride,
new { userAgent }, sessionId).ConfigureAwait(
false);
7189 public async Task PageDown(
int pagesToScroll = 1,
string sessionId =
null)
7191 sessionId ??= GetEffectiveSessionId();
7192 for (
int i = 0; i < pagesToScroll; i++)
7194 await ScrollPageAsync(
"down", pagesToScroll, sessionId).ConfigureAwait(
false);
7205 public async Task
PageEnd(
string sessionId =
null)
7218 public async Task PageTop(
string sessionId =
null)
7220 sessionId ??= GetEffectiveSessionId();
7221 await ScrollToPositionAsync(
"top", sessionId).ConfigureAwait(
false);
7232 public async Task PageUp(
int pagesToScroll = 1,
string sessionId =
null)
7234 sessionId ??= GetEffectiveSessionId();
7235 for (
int i = 0; i < pagesToScroll; i++)
7237 await ScrollPageAsync(
"up", pagesToScroll, sessionId).ConfigureAwait(
false);
7250 public async Task PressModifierKey(
int modifierKeys,
string sessionId =
null)
7252 sessionId ??= GetEffectiveSessionId();
7253 await SendCommand<object>(DevToolsMethods.InputDispatchKeyEvent,
new { type =
"keyDown", modifiers = modifierKeys }, sessionId).ConfigureAwait(
false);
7265 public async Task<string> PreviousTab(
string currentTargetId)
7267 var windowArray = _windowSessionsQueue.ToArray();
7268 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
7270 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"No active window for PreviousTab",
this, GPALObjectType.PuppeteerCommunicator);
7274 var session = windowArray[_activeWindowIndex];
7275 var tabQueue = session.TabQueue;
7276 var browserContextId = session.BrowserContextId;
7279 var targetsResponse = await SendCommand<dynamic>(
7280 DevToolsMethods.TargetGetTargets,
7283 ).ConfigureAwait(
false);
7285 if (targetsResponse?.targetInfos ==
null)
7287 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"Failed to get targets list in NextTab sync",
this, GPALObjectType.PuppeteerCommunicator);
7292 var knownTargetIds =
new HashSet<string>(tabQueue.Select(kv => kv.Key));
7294 foreach (var targetInfo
in targetsResponse.targetInfos)
7296 string targetId = targetInfo.targetId?.ToString();
7297 string type = targetInfo.type?.ToString();
7298 string contextId = targetInfo.browserContextId?.ToString();
7300 if (
string.IsNullOrEmpty(targetId) || type !=
"page" || contextId != browserContextId)
7303 if (!knownTargetIds.Contains(targetId))
7306 await AddTabToQueue(targetId,
null,
false,
true).ConfigureAwait(
false);
7307 knownTargetIds.Add(targetId);
7312 var queueArray = tabQueue.ToArray();
7313 if (queueArray.Length == 0)
7315 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"No tabs for PreviousTab",
this, GPALObjectType.PuppeteerCommunicator);
7319 var currentIndex = Array.FindIndex(queueArray, kvp => kvp.Key == currentTargetId);
7320 if (currentIndex == -1)
7322 currentIndex = windowArray[_activeWindowIndex].ActiveTabIndex;
7325 var prevIndex = (currentIndex - 1 + queueArray.Length) % queueArray.Length;
7326 var prevTargetId = queueArray[prevIndex].Key;
7327 var prevSessionId = queueArray[prevIndex].Value;
7330 var windowList = _windowSessionsQueue.ToList();
7332 windowList[_activeWindowIndex].BrowserContextId,
7336 _windowSessionsQueue =
new ConcurrentQueue<WindowSession>();
7337 foreach (var w
in windowList)
7339 _windowSessionsQueue.Enqueue(w);
7344 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId = prevTargetId },
null).ConfigureAwait(
false);
7346 if (await SendCommand<object>(DevToolsMethods.PageEnable,
new { }, prevSessionId).ConfigureAwait(
false) ==
null)
7348 var newSession = await SendCommand<object>(DevToolsMethods.TargetAttachToTarget,
new { targetId = prevTargetId, flatten = true },
null).ConfigureAwait(
false);
7349 if (newSession?.sessionId !=
null)
7351 prevSessionId = (string)newSession.sessionId;
7352 RemoveTabFromQueue(prevTargetId);
7353 await AddTabToQueue(prevTargetId, prevSessionId).ConfigureAwait(
false);
7357 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to re-attach PreviousTab ID [{prevTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
7360 await SendCommand<object>(DevToolsMethods.PageDisable,
new { }, prevSessionId).ConfigureAwait(
false);
7364 await SendCommand<object>(DevToolsMethods.PageDisable,
new { }, prevSessionId).ConfigureAwait(
false);
7367 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"PreviousTab: Switched to index [{prevIndex}], ID [{prevTargetId}], Active window tab queue size [{queueArray.Length}]",
this, GPALObjectType.PuppeteerCommunicator);
7368 return prevTargetId;
7381 var windowArray = _windowSessionsQueue.ToArray();
7382 if (windowArray.Length == 0)
7384 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No windows for PreviousWindow",
this, GPALObjectType.PuppeteerCommunicator);
7388 var currentWindowIndex = -1;
7389 for (
int i = 0; i < windowArray.Length; i++)
7391 var tabArray = windowArray[i].TabQueue.ToArray();
7392 if (Array.Exists(tabArray, kvp => kvp.Key == currentTargetId))
7394 currentWindowIndex = i;
7398 if (currentWindowIndex == -1)
7400 currentWindowIndex = _activeWindowIndex;
7403 var prevWindowIndex = (currentWindowIndex - 1 + windowArray.Length) % windowArray.Length;
7404 SetActiveWindowIndex(prevWindowIndex);
7406 var prevWindowContextId = windowArray[prevWindowIndex].BrowserContextId;
7407 var prevTabQueue = windowArray[prevWindowIndex].TabQueue;
7408 var prevTabArray = prevTabQueue.ToArray();
7409 if (prevTabArray.Length == 0)
7411 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"No tabs in previous window for PreviousWindow (context [{prevWindowContextId}])",
this, GPALObjectType.PuppeteerCommunicator);
7415 var prevTargetId = prevTabArray[windowArray[prevWindowIndex].ActiveTabIndex].Key;
7416 var prevSessionId = prevTabArray[windowArray[prevWindowIndex].ActiveTabIndex].Value;
7418 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId = prevTargetId },
null).ConfigureAwait(
false);
7420 if (await SendCommand<object>(DevToolsMethods.PageEnable,
new { }, prevSessionId).ConfigureAwait(
false) ==
null)
7422 var newSession = await SendCommand<object>(DevToolsMethods.TargetAttachToTarget,
new { targetId = prevTargetId, flatten = true },
null).ConfigureAwait(
false);
7423 if (newSession?.sessionId !=
null)
7425 prevSessionId = (string)newSession.sessionId;
7426 RemoveTabFromQueue(prevTargetId);
7427 await AddTabToQueue(prevTargetId, prevSessionId).ConfigureAwait(
false);
7431 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to re-attach PreviousWindow tab ID [{prevTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
7434 await SendCommand<object>(DevToolsMethods.PageDisable,
new { }, prevSessionId).ConfigureAwait(
false);
7438 await SendCommand<object>(DevToolsMethods.PageDisable,
new { }, prevSessionId).ConfigureAwait(
false);
7441 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"PreviousWindow: Switched to window index [{prevWindowIndex}], Context [{prevWindowContextId}], Tab ID [{prevTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
7442 return prevTargetId;
7451 public async Task<dynamic> QuerySelector(
string css,
string sessionId =
null)
7453 sessionId ??= GetEffectiveSessionId();
7454 return await QueryByCss(css, sessionId).ConfigureAwait(
false);
7464 internal async Task<long> DocumentMark(
string sessionId =
null)
7466 sessionId ??= GetEffectiveSessionId();
7468 var doc = await SendCommand<object>(DevToolsMethods.DOMGetDocument,
new { depth = 0 }, sessionId).ConfigureAwait(
false);
7469 long mark =
null == doc ||
null == doc.root ? 0 : (long)doc.root.backendNodeId;
7482 public async Task<dynamic> QuerySelectors(
string css,
string sessionId =
null)
7484 sessionId ??= GetEffectiveSessionId();
7486 var doc = await SendCommand<object>(DevToolsMethods.DOMGetDocument,
new { depth = 0 }, sessionId).ConfigureAwait(
false);
7488 return await SendCommand<object>(DevToolsMethods.DOMQuerySelectorAll,
new { nodeId = (int)doc.root.nodeId, selector = css }, sessionId).ConfigureAwait(
false);
7498 public async Task Refresh(
string sessionId =
null)
7500 sessionId ??= GetEffectiveSessionId();
7501 await SendCommand<object>(DevToolsMethods.PageReload,
new { }, sessionId).ConfigureAwait(
false);
7513 public async Task ReleaseModifierKey(
int modifierKeys,
string sessionId =
null)
7515 sessionId ??= GetEffectiveSessionId();
7516 await SendCommand<object>(DevToolsMethods.InputDispatchKeyEvent,
new { type =
"keyUp", modifiers = modifierKeys }, sessionId).ConfigureAwait(
false);
7526 public async Task<bool> Restore(
string sessionId =
null)
7528 sessionId ??= GetEffectiveSessionId();
7530 var windowId = await GetCurrentWindow(sessionId).ConfigureAwait(
false);
7531 await SendCommand(DevToolsMethods.BrowserSetWindowBounds,
new
7533 windowId = windowId,
7534 bounds = new { windowState =
"normal" }
7535 }, sessionId).ConfigureAwait(
false);
7554 public async Task MiddleClick(
string elementId,
string sessionId =
null)
7556 sessionId ??= GetEffectiveSessionId();
7557 List<GPALElement> elems = await EvaluateSelector(elementId, sessionId).ConfigureAwait(
false);
7558 List<dynamic> responses =
new List<dynamic>();
7560 foreach (GPALElement elem
in elems)
7562 await ScrollIntoView(elem, sessionId).ConfigureAwait(
false);
7563 await ClickElement(elem, sessionId, responses, ClickType.MiddleClick).ConfigureAwait(
false);
7581 public async Task RightClick(
string elementId,
string sessionId =
null)
7583 sessionId ??= GetEffectiveSessionId();
7584 List<GPALElement> elems = await EvaluateSelector(elementId, sessionId).ConfigureAwait(
false);
7585 List<dynamic> responses =
new List<dynamic>();
7587 foreach (GPALElement elem
in elems)
7589 await ScrollIntoView(elem, sessionId).ConfigureAwait(
false);
7590 await ClickElement(elem, sessionId, responses, ClickType.RightClick).ConfigureAwait(
false);
7604 public async Task RightClickAndDownload(
string elementId,
string savePath,
string sessionId =
null)
7606 await SendCommand<object>(DevToolsMethods.PageSetDownloadBehavior,
new { behavior =
"allow", downloadPath = savePath }, sessionId).ConfigureAwait(
false);
7607 await RightClick(elementId, sessionId).ConfigureAwait(
false);
7621 public async Task ScrollElement(
string elementId,
int hPixels,
int vPixels,
string sessionId =
null)
7623 sessionId ??= GetEffectiveSessionId();
7624 await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
7626 contextId = CurrentContextId,
7627 expression =
"document.getElementById('" + elementId +
"').scrollBy(" + hPixels +
", " + vPixels +
")"
7628 }, sessionId).ConfigureAwait(
false);
7648 public async Task<bool> ScrollIntoView(GPALElement element,
string sessionId =
null)
7650 int backendNodeId = element.ElementBackendNodeId;
7652 sessionId ??= GetEffectiveSessionId();
7656 if (
"GPALElement".Equals(element.TagName))
7660 if (
true == Browser.BrowserSettings.UseHeadless)
7663 await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
7665 contextId = CurrentContextId,
7667 document.body.style.zoom = '100%'; // minor reflow trigger
7668 window.dispatchEvent(new Event('resize'));
7669 document.documentElement.scrollTop += 1; document.documentElement.scrollTop -= 1;"
7670 }, sessionId).ConfigureAwait(
false);
7674 await SendCommand(DevToolsMethods.RuntimeEvaluate,
new
7677 if (!document.querySelector('style[data-gpal-overscroll]')) {
7678 const style = document.createElement('style');
7679 style.setAttribute('data-gpal-overscroll', 'true');
7680 style.textContent = `
7682 overscroll-behavior: none !important;
7683 overscroll-behavior-x: none !important;
7684 overscroll-behavior-y: none !important;
7687 (document.head || document.documentElement).appendChild(style);
7690 returnByValue = true
7691 }, sessionId).ConfigureAwait(
false);
7694 var scrollResult = await SendCommand<object>(
7695 DevToolsMethods.DOMScrollIntoViewIfNeeded,
7696 new { backendNodeId },
7698 ).ConfigureAwait(
false);
7700 if (
true == Browser.BrowserSettings.UseHeadless)
7701 await Task.Delay(250).ConfigureAwait(
false);
7703 if (
null == scrollResult)
7706 var escapedSelector = element.Css.Replace(
@"\",
@"\\").Replace(
"'",
@"\'");
7708 var callResult = await SendCommand<dynamic>(
7709 DevToolsMethods.RuntimeEvaluate,
7714 const el = document.querySelector(""{escapedSelector}"");
7716 console.warn('Element not found by selector: {escapedSelector}');
7720 el.scrollIntoView({{
7721 behavior: 'instant',
7726 // Force reflow/layout
7727 void el.getBoundingClientRect();
7728 void el.offsetHeight;
7730 return new Promise(r => requestAnimationFrame(() => r(true)));
7732 awaitPromise = true,
7733 contextId = CurrentContextId,
7734 returnByValue = true
7737 ).ConfigureAwait(
false);
7755 catch (Exception ex)
7758 $
"ScrollIntoView failed",
7759 this, GPALObjectType.PuppeteerCommunicator, ex);
7856 public async Task
ScrollWindow(
int hPixels,
int vPixels,
string sessionId =
null)
7860 await Task.Delay(150).ConfigureAwait(
false);
7875 public async Task<dynamic> SendKey(
string key,
string code,
int vkCode,
string sessionId =
null)
7877 sessionId ??= GetEffectiveSessionId();
7883 string textPayload = (code ==
"Enter") ?
"\r" : (key.Length == 1 ? key : null);
7886 var keyDownParams =
new
7888 type =
"rawKeyDown",
7891 windowsVirtualKeyCode = vkCode,
7892 nativeVirtualKeyCode = vkCode
7894 await SendCommand<object>(DevToolsMethods.InputDispatchKeyEvent, keyDownParams, sessionId).ConfigureAwait(
false);
7897 if (textPayload !=
null)
7899 var charParams =
new
7903 unmodifiedText = textPayload,
7906 windowsVirtualKeyCode = vkCode,
7907 nativeVirtualKeyCode = vkCode
7909 await SendCommand<object>(DevToolsMethods.InputDispatchKeyEvent, charParams, sessionId).ConfigureAwait(
false);
7913 var keyUpParams =
new
7918 windowsVirtualKeyCode = vkCode,
7919 nativeVirtualKeyCode = vkCode
7921 await SendCommand<dynamic>(DevToolsMethods.InputDispatchKeyEvent, keyUpParams, sessionId).ConfigureAwait(
false);
7923 catch (Exception ex)
7925 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"SendKey execution failed for Code [{code}]",
this, GPALObjectType.PuppeteerCommunicator, ex);
7937 private int GetVirtualKeyCode(
string key)
7939 if (
string.IsNullOrEmpty(key))
7942 string k = key.ToLowerInvariant();
7998 case "f1":
return 0x70;
7999 case "f2":
return 0x71;
8000 case "f3":
return 0x72;
8001 case "f4":
return 0x73;
8002 case "f5":
return 0x74;
8003 case "f6":
return 0x75;
8004 case "f7":
return 0x76;
8005 case "f8":
return 0x77;
8006 case "f9":
return 0x78;
8007 case "f10":
return 0x79;
8008 case "f11":
return 0x7A;
8009 case "f12":
return 0x7B;
8029 public async Task SendString(
string text,
int delayMs = 0,
string sessionId =
null)
8031 sessionId ??= GetEffectiveSessionId();
8033 await SendCommand<object>(DevToolsMethods.InputInsertText,
new { text }, sessionId).ConfigureAwait(
false);
8035 await DispatchTextCharByChar(text, delayMs, sessionId).ConfigureAwait(
false);
8047 private async Task DispatchTextCharByChar(
string text,
int delayMs,
string sessionId)
8050 foreach (
char c
in text)
8052 if (
false == first && 0 < delayMs)
8053 await Task.Delay(HardwareHelper.GetTypingDelay(delayMs)).ConfigureAwait(
false);
8055 string key = c.ToString();
8056 int vkCode = (int)c;
8058 await SendCommand(DevToolsMethods.InputDispatchKeyEvent,
new
8062 unmodifiedtext = key,
8064 code = GetKeyCode(c),
8065 windowsVirtualKeyCode = vkCode,
8066 nativeVirtualKeyCode = vkCode
8067 }, sessionId).ConfigureAwait(
false);
8069 await SendCommand(DevToolsMethods.InputDispatchKeyEvent,
new
8073 code = GetKeyCode(c),
8074 windowsVirtualKeyCode = vkCode
8075 }, sessionId).ConfigureAwait(
false);
8091 public async Task<dynamic> SetAttribute(
int backendNodeId,
string attribute,
string value,
string sessionId =
null)
8093 sessionId ??= GetEffectiveSessionId();
8094 var nodeId = await GetNodeIdFromBackendNodeId(backendNodeId, sessionId).ConfigureAwait(
false);
8095 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Set attribute [{attribute}] to [{value}] on backendNodId [{backendNodeId}]",
this, GPALObjectType.PuppeteerCommunicator);
8096 return await SendCommand<object>(DevToolsMethods.DOMSetAttributeValue,
new { nodeId, name = attribute, value }, sessionId).ConfigureAwait(
false);
8107 public async Task SetDownloadFilename(
string downloadPath,
string sessionId =
null)
8109 sessionId ??= GetEffectiveSessionId();
8110 await SendCommand<object>(DevToolsMethods.PageSetDownloadBehavior,
new { behavior =
"allow", downloadPath }, sessionId).ConfigureAwait(
false);
8123 public async Task<dynamic> SetRange(
string elementId,
string rangeValue,
string sessionId =
null)
8125 sessionId ??= GetEffectiveSessionId();
8126 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8129 contextId = CurrentContextId,
8130 expression =
$@"var s = document.querySelector('{elementId}'); s.value = {rangeValue}; s.dispatchEvent(new Event('input', {{bubbles: true}})); s.dispatchEvent(new Event('change', {{bubbles: true}}));"
8131 }, sessionId).ConfigureAwait(
false);
8141 public async Task TopBrowser(
string sessionId =
null)
8146 await SendCommand<object>(DevToolsMethods.PageBringToFront,
new { }, GetCurrentSessionId()).ConfigureAwait(
false);
8161 public async Task<bool> SetStorage(
8167 string storeName =
null,
8170 bool retVal =
false;
8175 switch (storageType)
8179 IDictionary<string, object> cookieToSet;
8185 dynamic cookiesResp = await
GetStorage(storageType, sessionId, domain, storeName, path, key).ConfigureAwait(
false);
8188 dynamic existing =
null;
8190 if (cookiesResp !=
null)
8192 var cookies = JsonConvert.DeserializeObject<List<dynamic>>(cookiesResp) ??
new List<dynamic>();
8193 foreach (dynamic c
in cookies)
8195 string cDomain = ((dynamic)c).Domain ??
"";
8196 string cPath = ((dynamic)c).Path ??
"/";
8197 string cName = ((dynamic)c).Key ??
"";
8200 UrlHelper.CookieDomainMatches(cDomain, domain) &&
8205 cookieToSet =
new System.Dynamic.ExpandoObject() as IDictionary<string, object>;
8207 cookieToSet[
"name"] = key;
8208 cookieToSet[
"value"] = data;
8209 cookieToSet[
"domain"] = ((dynamic)existing)?.Domain?.ToString();
8210 cookieToSet[
"path"] = path;
8212 cookieToSet[
"expires"] = ((dynamic)existing)?.Expires !=
null
8213 ? (
double)existing.Expires
8214 : DateTimeOffset.UtcNow.ToUnixTimeSeconds() + (180L * 24 * 60 * 60);
8216 cookieToSet[
"secure"] = ((dynamic)existing)?.Secure ??
true;
8217 cookieToSet[
"httpOnly"] = ((dynamic)existing)?.HttpOnly ??
true;
8218 cookieToSet[
"sameSite"] = ((dynamic)existing)?.SameSite?.ToString() ??
"None";
8220 cookieToSet[
"session"] = ((dynamic)existing)?.Session ??
false;
8221 cookieToSet[
"priority"] = ((dynamic)existing)?.Priority?.ToString() ??
"Medium";
8223 cookieToSet[
"sourceScheme"] = ((dynamic)existing)?.SourceScheme?.ToString() ??
"Secure";
8224 cookieToSet[
"sourcePort"] = ((dynamic)existing)?.SourcePort ?? 443;
8227 if (((dynamic)existing)?.PartitionKey !=
null)
8229 cookieToSet[
"partitionKey"] = existing.PartitionKey;
8233 await SendCommand<dynamic>(DevToolsMethods.StorageSetCookies,
8234 new { cookies = new[] { cookieToSet } },
8235 sessionId).ConfigureAwait(
false);
8242 cookieToSet =
new System.Dynamic.ExpandoObject() as IDictionary<string, object>;
8244 cookieToSet[
"name"] = key;
8245 cookieToSet[
"value"] = data;
8246 cookieToSet[
"domain"] = ((dynamic)existing)?.Domain?.ToString();
8247 cookieToSet[
"path"] = path;
8249 cookieToSet[
"expires"] = ((dynamic)existing)?.Expires !=
null
8250 ? (
double)existing.Expires
8251 : DateTimeOffset.UtcNow.ToUnixTimeSeconds() + (180L * 24 * 60 * 60);
8253 cookieToSet[
"secure"] = ((dynamic)existing)?.Secure ??
true;
8254 cookieToSet[
"httpOnly"] = ((dynamic)existing)?.HttpOnly ??
true;
8255 cookieToSet[
"sameSite"] = ((dynamic)existing)?.SameSite?.ToString() ??
"None";
8257 cookieToSet[
"session"] = ((dynamic)existing)?.Session ??
false;
8258 cookieToSet[
"priority"] = ((dynamic)existing)?.Priority?.ToString() ??
"Medium";
8260 cookieToSet[
"sourceScheme"] = ((dynamic)existing)?.SourceScheme?.ToString() ??
"Secure";
8261 cookieToSet[
"sourcePort"] = ((dynamic)existing)?.SourcePort ?? 443;
8264 if (((dynamic)existing)?.PartitionKey !=
null)
8266 cookieToSet[
"partitionKey"] = existing.PartitionKey;
8270 await SendCommand<dynamic>(DevToolsMethods.StorageSetCookies,
8271 new { cookies = new[] { cookieToSet } },
8272 sessionId).ConfigureAwait(
false);
8281 case "localStorage":
8282 case "sessionStorage":
8283 string jsStorage =
$@"{storageType}.setItem('{EscapeJsString(key)}','{EscapeJsString(data)}');";
8284 await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
8286 expression = jsStorage,
8287 awaitPromise = false,
8288 contextId = CurrentContextId,
8289 returnByValue = true
8290 }, sessionId).ConfigureAwait(
false);
8296 var callback = arguments[arguments.length - 1];
8299 const dbName = '{EscapeJsString(path)}';
8300 const storeName = '{EscapeJsString(storeName)}';
8301 const inputKey = '{EscapeJsString(key)}';
8302 let rawValue = '{EscapeJsString(data)}';
8304 function resolve(val) {{
8308 function fail(msg, err, db) {{
8309 console.error(msg, err?.name || err || '');
8310 try {{ db?.close(); }} catch {{ }}
8314 function parseValue(v) {{
8316 return JSON.parse(v);
8322 function isValidKey(k) {{
8323 if (k === undefined) return false;
8324 if (typeof k === 'string' || typeof k === 'number') return true;
8325 if (k instanceof Date) return true;
8326 if (Array.isArray(k)) return true;
8330 function ensureInlineKey(value, keyPath, key, autoIncrement) {{
8331 if (keyPath === null) return {{ value, usedKey: key }};
8333 if (typeof value !== 'object' || value === null) {{
8334 if (keyPath && key !== undefined) {{
8335 const wrapped = {{}};
8337 if (typeof keyPath === 'string') {{
8338 wrapped[keyPath] = key;
8339 }} else if (Array.isArray(keyPath)) {{
8340 if (!Array.isArray(key)) {{
8341 throw new Error('Compound key requires array key');
8343 for (let i = 0; i < keyPath.length; i++) {{
8344 wrapped[keyPath[i]] = key[i];
8348 wrapped.value = value;
8349 return {{ value: wrapped, usedKey: undefined }};
8352 if (autoIncrement) {{
8353 return {{ value, usedKey: undefined }};
8356 throw new Error('Inline keyPath requires object value');
8359 if (typeof keyPath === 'string') {{
8360 if (!(keyPath in value)) {{
8361 if (key !== undefined) {{
8362 value[keyPath] = key;
8363 }} else if (!autoIncrement) {{
8364 throw new Error('Missing required inline keyPath property');
8367 }} else if (Array.isArray(keyPath)) {{
8368 for (let i = 0; i < keyPath.length; i++) {{
8369 const kp = keyPath[i];
8370 if (!(kp in value)) {{
8371 if (Array.isArray(key) && key[i] !== undefined) {{
8373 }} else if (!autoIncrement) {{
8374 throw new Error('Missing compound keyPath property: ' + kp);
8380 return {{ value, usedKey: undefined }};
8383 const openReq = indexedDB.open(dbName);
8385 openReq.onerror = () => {{
8386 fail('Failed to open database', openReq.error);
8389 openReq.onsuccess = () => {{
8390 const db = openReq.result;
8394 tx = db.transaction(storeName, 'readwrite');
8396 fail('Transaction creation failed', e, db);
8400 const store = tx.objectStore(storeName);
8402 let value = parseValue(rawValue);
8405 if (key === '') key = undefined;
8407 if (key !== undefined && !isValidKey(key)) {{
8408 fail('Invalid key type', key, db);
8412 let finalValue, finalKey;
8415 const result = ensureInlineKey(
8422 finalValue = result.value;
8423 finalKey = result.usedKey;
8425 fail('Key handling failed', e, db);
8432 if (store.keyPath === null) {{
8433 if (finalKey === undefined && !store.autoIncrement) {{
8434 fail('Missing key for out-of-line store', null, db);
8437 putReq = store.put(finalValue, finalKey);
8439 putReq = store.put(finalValue);
8442 fail('Put setup error', e, db);
8446 // wait for transaction complete (not just put)
8447 tx.oncomplete = () => {{
8448 try {{ db.close(); }} catch {{ }}
8452 tx.onerror = (event) => {{
8453 fail('Transaction error', event.target.error, db);
8456 tx.onabort = (event) => {{
8457 fail('Transaction aborted', event.target.error, db);
8463 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
8466 awaitPromise = true,
8467 contextId = CurrentContextId,
8468 returnByValue = true
8469 }, sessionId).GetAwaiter().GetResult()?.ToString().Contains(
"true");
8474 string cacheName = EscapeJsString(storeName);
8475 string requestUrl = EscapeJsString(key);
8480 const cache = await caches.open('{cacheName}').ConfigureAwait(false);
8482 const request = new Request('{requestUrl}', {{
8486 const response = new Response('{data}', {{
8490 'Content-Type': 'text/plain;charset=utf-8',
8491 'Cache-Control': 'max-age=31536000' // or make this configurable
8495 await cache.put(request, response).ConfigureAwait(false);
8496 return {{ success: true, cacheName: '{cacheName}', key: '{requestUrl}' }};
8498 console.error('Cache set failed:', err);
8499 return {{ success: false, error: err.message }};
8504 var evalResult = await SendCommand<dynamic>(
8505 DevToolsMethods.RuntimeEvaluate,
8509 awaitPromise = true,
8510 contextId = CurrentContextId,
8511 returnByValue = true
8513 sessionId).ConfigureAwait(
false);
8516 retVal = evalResult?.result?.value?.success ==
true;
8521 GPALEventType.ERROR,
8522 $
"Unsupported storage type: [{storageType}]",
8524 GPALObjectType.PuppeteerCommunicator);
8528 catch (Exception ex)
8531 GPALEventType.EXCEPTION,
8532 $
"SetStorage failed for [{storageType}] domain=[{domain}] storeName=[{storeName}] path=[{path}] key=[{key}]",
8534 GPALObjectType.PuppeteerCommunicator,
8552 public async Task<dynamic> SetValueFromElement(
string srcCss,
string destCss,
string sessionId =
null)
8554 sessionId ??= GetEffectiveSessionId();
8555 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8558 contextId = CurrentContextId,
8559 expression =
$@"var s = document.querySelector('{srcCss}'); var d = document.querySelector('{destCss}'); d.value = s.value; d.dispatchEvent(new Event('input', {{bubbles: true}})); d.dispatchEvent(new Event('change', {{bubbles: true}}));"
8560 }, sessionId).ConfigureAwait(
false);
8571 public async Task StealthOverrideReferrer(
string sessionId =
null)
8573 sessionId ??= GetEffectiveSessionId();
8574 await SendCommand<object>(DevToolsMethods.NetworkSetExtraHTTPHeaders,
new { headers = new { Referer =
"https://www.google.com" } }, sessionId).ConfigureAwait(
false);
8584 public async Task<dynamic> SubmitForm(
string objectId,
string sessionId =
null)
8586 sessionId ??= GetEffectiveSessionId();
8587 return await SendCommand<object>(DevToolsMethods.RuntimeCallFunctionOn,
new
8590 contextId = CurrentContextId,
8591 functionDeclaration =
"function() {{ this.submit(); }}"
8592 }, sessionId).ConfigureAwait(
false);
8601 public Task SwitchToDefaultContent(
string sessionId =
null)
8605 CurrentContextId =
null;
8606 CurrentFrameId =
null;
8607 CurrentFrameSessionId =
null;
8608 CurrentRootObjectId =
null;
8610 return Task.CompletedTask;
8620 public async Task<string> GoToWindow(
object tabIdOrUrl,
string sessionId =
null)
8622 string targetId =
null;
8624 if (tabIdOrUrl is
int tabId)
8626 var queueArray = CurrentSessions.ToArray();
8627 if (tabId >= 0 && tabId < queueArray.Length)
8629 targetId = queueArray[tabId].Key;
8631 SetActiveTabIndex(tabId);
8634 else if (tabIdOrUrl is
string url && !
string.IsNullOrWhiteSpace(url))
8636 targetId = await GetTargetTabIdByUrl(url, sessionId).ConfigureAwait(
false);
8637 if (!
string.IsNullOrEmpty(targetId))
8640 int windowIndex = GetCurrentWindowIndex(targetId);
8641 if (windowIndex >= 0)
8643 SetActiveWindowIndex(windowIndex);
8644 var windowArray = _windowSessionsQueue.ToArray();
8645 var tabQueue = windowArray[windowIndex].TabQueue.ToArray();
8646 int tabIndex = Array.FindIndex(tabQueue, kvp => kvp.Key == targetId);
8649 SetActiveTabIndex(tabIndex);
8655 if (
string.IsNullOrEmpty(targetId))
8657 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No target found for tabIdOrUrl [{tabIdOrUrl}]",
this, GPALObjectType.PuppeteerCommunicator);
8661 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId },
null).ConfigureAwait(
false);
8662 ((PuppeteerClient)PuppeteerClient)._currentTargetId = targetId;
8671 public async Task<int> WindowInnerHeight(
string sessionId =
null)
8673 sessionId ??= GetEffectiveSessionId();
8674 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8676 expression =
"window.innerHeight",
8677 contextId = CurrentContextId,
8678 returnByValue = true
8679 }, sessionId).ConfigureAwait(
false);
8680 return (
int)result.value;
8692 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8694 expression =
"window.innerWidth",
8695 contextId = CurrentContextId,
8696 returnByValue = true
8697 }, sessionId).ConfigureAwait(
false);
8698 return (
int)result.value;
8710 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8712 expression =
"window.outerHeight",
8713 contextId = CurrentContextId,
8714 returnByValue = true
8715 }, sessionId).ConfigureAwait(
false);
8716 return (
int)result.value;
8725 public async Task<int> WindowOuterWidth(
string sessionId =
null)
8727 sessionId ??= GetEffectiveSessionId();
8728 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8730 expression =
"window.outerWidth",
8731 contextId = CurrentContextId,
8732 returnByValue = true
8733 }, sessionId).ConfigureAwait(
false);
8734 return (
int)result.value;
8751 public async Task<string> Fetch(
string url,
string method =
null,
string body =
null,
string contentType =
null,
string[] headers =
null,
bool asBytes =
false,
string sessionId =
null)
8753 sessionId ??= GetEffectiveSessionId();
8755 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8760 {BrowserHelper.FetchSetupScript(url, method, body, contentType, headers, asBytes)}
8763 var response = await fetch(target.toString(), options);
8764 return await gpalEnvelope(response);
8769 contextId = CurrentContextId,
8770 returnByValue = true,
8772 }, sessionId).ConfigureAwait(
false);
8776 return (result as Newtonsoft.Json.Linq.JObject)[
"result"][
"value"]?.ToString();
8785 public async Task<int> WindowPageOffsetX(
string sessionId =
null)
8787 sessionId ??= GetEffectiveSessionId();
8788 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8790 expression =
"window.pageXOffset",
8791 contextId = CurrentContextId,
8792 returnByValue = true
8793 }, sessionId).ConfigureAwait(
false);
8794 return (
int)result.value;
8803 public async Task<int> WindowPageOffsetY(
string sessionId =
null)
8805 sessionId ??= GetEffectiveSessionId();
8806 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8808 expression =
"window.pageYOffset",
8809 contextId = CurrentContextId,
8810 returnByValue = true
8811 }, sessionId).ConfigureAwait(
false);
8812 return (
int)result.value;
8821 public async Task<int> WindowScreenLeft(
string sessionId =
null)
8823 sessionId ??= GetEffectiveSessionId();
8824 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8826 expression =
"window.screenLeft",
8827 contextId = CurrentContextId,
8828 returnByValue = true
8829 }, sessionId).ConfigureAwait(
false);
8830 return (
int)result.value;
8839 public async Task<int> WindowScreenTop(
string sessionId =
null)
8841 sessionId ??= GetEffectiveSessionId();
8842 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8844 expression =
"window.screenTop",
8845 contextId = CurrentContextId,
8846 returnByValue = true
8847 }, sessionId).ConfigureAwait(
false);
8848 return (
int)result.value;
8859 private async Task<int> GetNodeIdFromBackendNodeId(
int backendNodeId,
string sessionId =
null)
8861 sessionId ??= GetEffectiveSessionId();
8864 await SendCommand<dynamic>(DevToolsMethods.DOMGetDocument,
new { depth = 1 }, sessionId).ConfigureAwait(
false);
8867 var result = await SendCommand<dynamic>(
8868 DevToolsMethods.DOMPushNodesByBackendIdsToFrontend,
8869 new { backendNodeIds = new[] { backendNodeId } },
8871 ).ConfigureAwait(
false);
8874 if (result?.nodeIds is Newtonsoft.Json.Linq.JArray nodeIdsArray && nodeIdsArray.Count > 0)
8876 return nodeIdsArray[0].Value<
int>();
8884 public async Task<string> GetObjectIdFromBackendNodeId(
int backendNodeId,
string sessionId =
null,
long? contextId =
null)
8886 sessionId ??= GetEffectiveSessionId();
8889 string effectiveSessionId = !
string.IsNullOrEmpty(CurrentFrameSessionId)
8890 ? CurrentFrameSessionId
8894 long? effectiveContextId = contextId ?? CurrentContextId;
8899 var resolveNodeParams =
new Dictionary<string, object>
8901 [
"backendNodeId"] = backendNodeId
8905 if (effectiveContextId.HasValue)
8907 resolveNodeParams[
"executionContextId"] = effectiveContextId.Value;
8910 var resolveResult = await SendCommand<dynamic>(DevToolsMethods.DOMResolveNode, resolveNodeParams, effectiveSessionId).ConfigureAwait(
false);
8912 string objectId = resolveResult?.@
object?.objectId?.ToString();
8914 if (!
string.IsNullOrEmpty(objectId))
8918 var fallbackResult = await SendCommand<dynamic>(DevToolsMethods.DOMResolveNode,
new
8920 backendNodeId = backendNodeId
8921 }, effectiveSessionId).ConfigureAwait(
false);
8923 return fallbackResult?.@
object?.objectId?.ToString() ??
string.Empty;
8925 catch (Exception ex)
8927 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
8928 $
"GetObjectIdFromBackendNodeId failed for backendNodeId [{backendNodeId}]",
8929 this, GPALObjectType.PuppeteerCommunicator, ex);
8931 return string.Empty;
8943 private async Task<int> GetElementRandomCenterX(
int backendNodeId,
string sessionId =
null)
8945 sessionId ??= GetEffectiveSessionId();
8946 var rect = await GetBoundingClientRect(backendNodeId, sessionId).ConfigureAwait(
false);
8947 return (
int)rect.X + rect.Width /
new Random().Next(2, 3);
8958 private async Task<int> GetElementRandomCenterY(
int backendNodeId,
string sessionId =
null)
8960 sessionId ??= GetEffectiveSessionId();
8961 var rect = await GetBoundingClientRect(backendNodeId, sessionId).ConfigureAwait(
false);
8962 return (
int)rect.Y + rect.Width /
new Random().Next(2, 3);
8970 internal string GetProtocolJSON()
8972 using var http =
new System.Net.Http.HttpClient();
8973 var json = http.GetStringAsync($
"{_puppeteerUrl}/json/protocol").Result;
8974 return Newtonsoft.Json.Linq.JObject.Parse(json).ToString();
8980 internal string GetActiveWindowId()
8982 var windows = _windowSessionsQueue.ToArray();
8983 if (_activeWindowIndex < 0 || _activeWindowIndex >= windows.Length)
8985 return windows[_activeWindowIndex].BrowserContextId;
8992 private ConcurrentQueue<KeyValuePair<string, string>> GetActiveWindowTabQueue()
8994 var windowArray = _windowSessionsQueue.ToArray();
8995 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
return null;
8996 return windowArray[_activeWindowIndex].TabQueue;
9004 public string GetEffectiveSessionId()
9006 return !
string.IsNullOrEmpty(CurrentFrameSessionId)
9007 ? CurrentFrameSessionId
9008 : GetCurrentSessionId();
9024 internal void ClearCurrentSessionId()
9026 var windowArray = _windowSessionsQueue.ToArray();
9027 if (0 == windowArray.Length || _activeWindowIndex >= windowArray.Length)
9030 var window = windowArray[_activeWindowIndex];
9031 var tabArray = window.TabQueue.ToArray();
9032 if (0 == tabArray.Length || window.ActiveTabIndex >= tabArray.Length)
9035 string targetId = tabArray[window.ActiveTabIndex].Key;
9036 var newQueue =
new ConcurrentQueue<KeyValuePair<string, string>>();
9038 foreach (var kvp
in tabArray)
9039 newQueue.Enqueue(kvp.Key == targetId ?
new KeyValuePair<string, string>(kvp.Key,
null) : kvp);
9041 window.TabQueue = newQueue;
9043 public string GetCurrentSessionId()
9045 var windowArray = _windowSessionsQueue.ToArray();
9046 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
return null;
9048 var window = windowArray[_activeWindowIndex];
9049 var tabQueue = window.TabQueue;
9050 var tabArray = tabQueue.ToArray();
9051 var activeTabIndex = window.ActiveTabIndex;
9052 if (tabArray.Length == 0 || activeTabIndex >= tabArray.Length)
return null;
9054 var targetId = tabArray[activeTabIndex].Key;
9055 var sessionId = tabArray[activeTabIndex].Value;
9057 if (sessionId ==
null)
9059 var attachResult = SendCommand<dynamic>(DevToolsMethods.TargetAttachToTarget,
new
9063 },
null).GetAwaiter().GetResult();
9065 string newSessionId = attachResult.sessionId;
9068 var newQueue =
new ConcurrentQueue<KeyValuePair<string, string>>();
9069 foreach (var kvp
in tabArray)
9070 newQueue.Enqueue(kvp.Key == targetId
9071 ?
new KeyValuePair<string, string>(kvp.Key, newSessionId)
9074 window.TabQueue = newQueue;
9075 sessionId = newSessionId;
9086 public string GetCurrentTargetId()
9088 var windowArray = _windowSessionsQueue.ToArray();
9089 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
return null;
9090 var tabQueue = windowArray[_activeWindowIndex].TabQueue;
9091 var tabArray = tabQueue.ToArray();
9092 var activeTabIndex = windowArray[_activeWindowIndex].ActiveTabIndex;
9093 if (tabArray.Length == 0 || activeTabIndex >= tabArray.Length)
return null;
9094 return tabArray[activeTabIndex].Key;
9101 public string GetCurrentWindowTargetId()
9103 var windowArray = _windowSessionsQueue.ToArray();
9105 return windowArray[_activeWindowIndex].BrowserContextId;
9117 internal async Task AddTabToQueue(
string targetId,
string sessionId,
bool isActive =
true,
bool doNotGetSendSemaphore =
false)
9119 var windowArray = _windowSessionsQueue.ToArray();
9120 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
9123 var targets = await SendCommand<object>(DevToolsMethods.TargetGetTargets,
new { },
null, 3, doNotGetSendSemaphore).ConfigureAwait(
false);
9124 var target = ((IEnumerable<dynamic>)targets.targetInfos)
9125 .FirstOrDefault(t => t.targetId == targetId && t.type ==
"page");
9129 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Target ID [{targetId}] not found in CDP",
this, GPALObjectType.PuppeteerCommunicator);
9133 string browserContextId = target.browserContextId?.ToString() ??
"";
9135 var newTabQueue =
new ConcurrentQueue<KeyValuePair<string, string>>();
9136 newTabQueue.Enqueue(
new KeyValuePair<string, string>(targetId, sessionId));
9137 _windowSessionsQueue.Enqueue(
new WindowSession(browserContextId, newTabQueue, 0));
9138 _activeWindowIndex = 0;
9139 ((PuppeteerClient)PuppeteerClient)._currentTargetId = targetId;
9140 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Created window with browserContextId [{browserContextId}], added tab ID [{targetId}], Active window [0], tab [0]",
this, GPALObjectType.PuppeteerCommunicator);
9144 var tabQueue = windowArray[_activeWindowIndex].TabQueue;
9145 tabQueue.Enqueue(
new KeyValuePair<string, string>(targetId, sessionId));
9148 var windowList = _windowSessionsQueue.ToList();
9150 windowList[_activeWindowIndex].BrowserContextId,
9152 true == isActive ? tabQueue.Count - 1 : windowList[_activeWindowIndex].ActiveTabIndex
9154 _windowSessionsQueue =
new ConcurrentQueue<WindowSession>();
9155 foreach (var w
in windowList)
9157 _windowSessionsQueue.Enqueue(w);
9159 var newTabIdx = tabQueue.Count - 1;
9161 if (
true == isActive)
9164 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Added tab to active window: ID [{targetId}], Active tab index [{newTabIdx}], Tab queue size [{tabQueue.Count}]",
this, GPALObjectType.PuppeteerCommunicator);
9171 internal void SetActiveTabIndex(
int index)
9173 var windowArray = _windowSessionsQueue.ToArray();
9174 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
9176 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No active window for SetActiveTabIndex",
this, GPALObjectType.PuppeteerCommunicator);
9179 var tabQueue = windowArray[_activeWindowIndex].TabQueue;
9180 var tabArray = tabQueue.ToArray();
9181 index = index % tabArray.Length;
9182 if (index < 0) index += tabArray.Length;
9185 var windowList = _windowSessionsQueue.ToList();
9186 windowList[_activeWindowIndex] =
new WindowSession(
9187 windowList[_activeWindowIndex].BrowserContextId,
9188 windowList[_activeWindowIndex].TabQueue,
9191 _windowSessionsQueue =
new ConcurrentQueue<WindowSession>();
9192 foreach (var w
in windowList)
9194 _windowSessionsQueue.Enqueue(w);
9197 ((PuppeteerClient)PuppeteerClient)._currentTargetId = tabArray[index].Key;
9198 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Set active tab index [{index}] in active window, ID [{tabArray[index].Key}]",
this, GPALObjectType.PuppeteerCommunicator);
9207 internal bool RemoveTabFromQueue(
string targetId)
9209 var windowArray = _windowSessionsQueue.ToArray();
9210 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
9212 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No active window for RemoveTabFromQueue [{targetId}]",
this, GPALObjectType.PuppeteerCommunicator);
9216 var tabQueue = windowArray[_activeWindowIndex].TabQueue;
9217 var queueList = tabQueue.ToList();
9218 var indexToRemove = queueList.FindIndex(kvp => kvp.Key == targetId);
9219 if (indexToRemove == -1)
9221 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Tab ID [{targetId}] not found in active window queue",
this, GPALObjectType.PuppeteerCommunicator);
9225 queueList.RemoveAt(indexToRemove);
9226 var newTabQueue =
new ConcurrentQueue<KeyValuePair<string, string>>();
9227 foreach (var kvp
in queueList)
9229 newTabQueue.Enqueue(kvp);
9233 var newActiveTabIndex = windowArray[_activeWindowIndex].ActiveTabIndex;
9234 if (newTabQueue.IsEmpty)
9236 newActiveTabIndex = 0;
9238 else if (newActiveTabIndex > indexToRemove)
9240 newActiveTabIndex--;
9242 else if (newActiveTabIndex == indexToRemove)
9244 newActiveTabIndex = Math.Max(0, newActiveTabIndex - 1);
9247 var windowList = _windowSessionsQueue.ToList();
9248 windowList[_activeWindowIndex] =
new WindowSession(
9249 windowList[_activeWindowIndex].BrowserContextId,
9253 _windowSessionsQueue =
new ConcurrentQueue<WindowSession>();
9254 foreach (var w
in windowList)
9256 _windowSessionsQueue.Enqueue(w);
9259 ((PuppeteerClient)PuppeteerClient)._currentTargetId = newTabQueue.IsEmpty ?
null : newTabQueue.ToArray()[newActiveTabIndex].Key;
9260 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Removed tab ID [{targetId}] from active window, Active tab index now [{newActiveTabIndex}], Tab queue size [{newTabQueue.Count}]",
this, GPALObjectType.PuppeteerCommunicator);
9269 public int GetCurrentWindowIndex(
string targetId)
9271 var windows = _windowSessionsQueue.ToArray();
9272 for (
int i = 0; i < windows.Length; i++)
9274 foreach (var tab
in windows[i].TabQueue)
9275 if (tab.Key == targetId)
9286 internal void SetActiveWindowIndex(
int index)
9288 var windowArray = _windowSessionsQueue.ToArray();
9289 if (windowArray.Length == 0)
9291 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No windows for SetActiveWindowIndex",
this, GPALObjectType.PuppeteerCommunicator);
9294 index = index % windowArray.Length;
9295 if (index < 0) index += windowArray.Length;
9296 _activeWindowIndex = index;
9299 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Set active window index [{_activeWindowIndex}], Active tab index [{windowArray[_activeWindowIndex].ActiveTabIndex}], Target ID [{((PuppeteerClient)PuppeteerClient)._currentTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
9309 internal bool RemoveWindowFromQueue(
string browserContextId =
null,
string targetId =
null)
9311 var windowArray = _windowSessionsQueue.ToArray();
9312 var indexToRemove = -1;
9314 if (browserContextId !=
null)
9316 indexToRemove = Array.FindIndex(windowArray, w => w.BrowserContextId == browserContextId);
9318 else if (targetId !=
null)
9320 for (
int i = 0; i < windowArray.Length; i++)
9322 var tabArray = windowArray[i].TabQueue.ToArray();
9323 if (Array.Exists(tabArray, kvp => kvp.Key == targetId))
9326 browserContextId = windowArray[i].BrowserContextId;
9332 if (indexToRemove == -1)
9334 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Window/Context ID [{browserContextId ?? targetId}] not found",
this, GPALObjectType.PuppeteerCommunicator);
9338 var queueList = _windowSessionsQueue.ToList();
9339 queueList.RemoveAt(indexToRemove);
9341 _windowSessionsQueue =
new ConcurrentQueue<WindowSession>();
9342 foreach (var w
in queueList)
9344 _windowSessionsQueue.Enqueue(w);
9347 if (queueList.Count == 0)
9349 _activeWindowIndex = 0;
9350 ((PuppeteerClient)PuppeteerClient)._currentTargetId =
null;
9351 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"No windows remain after removing window with browserContextId [{browserContextId}]",
this, GPALObjectType.PuppeteerCommunicator);
9355 if (_activeWindowIndex > indexToRemove)
9357 _activeWindowIndex--;
9359 else if (_activeWindowIndex == indexToRemove)
9361 _activeWindowIndex = Math.Max(0, _activeWindowIndex - 1);
9363 ((PuppeteerClient)PuppeteerClient)._currentTargetId = GetCurrentTargetId();
9364 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Removed window with browserContextId [{browserContextId}], Active window index now [{_activeWindowIndex}], Active tab index [{windowArray[_activeWindowIndex].ActiveTabIndex}], New active target [{((PuppeteerClient)PuppeteerClient)._currentTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
9378 public async Task<bool> ScrollPageAsync(
string direction,
int pagesToScroll,
string s)
9380 double initialScrollY = 0;
9381 bool retVal =
false;
9386 if (direction !=
"up" && direction !=
"down")
9392 var overflowResult = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
9394 expression =
@"getComputedStyle(document.documentElement).overflow",
9395 contextId = CurrentContextId,
9396 returnByValue = true
9397 }, s).ConfigureAwait(
false);
9398 string overflow = overflowResult?[
"result"]?[
"value"].ToString();
9401 if (overflow !=
"hidden")
9403 for (
int count = 0; count < pagesToScroll; count++)
9405 if (
true == (retVal = await scrollPage().ConfigureAwait(
false)))
9411 if (
false == retVal)
9412 for (
int count = 0; count < pagesToScroll; count++)
9415 if (
true == (retVal = await scrollSinglePageApp().ConfigureAwait(
false)))
9421 catch (Exception ex)
9423 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Unable to page [{direction}] for [{pagesToScroll}] pages.",
this, GPALObjectType.PuppeteerClient, ex);
9426 async Task<bool> scrollPage()
9429 var initialScrollResult = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
9431 expression =
@"window.scrollY",
9432 contextId = CurrentContextId,
9433 returnByValue = true
9434 }, s).ConfigureAwait(
false);
9435 initialScrollY = Convert.ToDouble(initialScrollResult?[
"result"]?[
"value"]);
9438 string scrollDirection = direction ==
"down" ?
"window.innerHeight" :
"-window.innerHeight";
9439 await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
new
9441 expression = $
"window.scrollBy({{ left: 0, top: {scrollDirection}, behavior: 'instant' }})",
9442 contextId = CurrentContextId,
9443 returnByValue = true
9444 }, s).ConfigureAwait(
false);
9447 var afterScrollResult = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
9449 expression =
@"window.scrollY",
9450 contextId = CurrentContextId,
9451 returnByValue = true
9453 }, s).ConfigureAwait(
false);
9454 double afterScrollY = Convert.ToDouble(afterScrollResult?[
"result"]?[
"value"]);
9456 if (afterScrollY != initialScrollY)
9463 async Task<bool> scrollSinglePageApp()
9466 var elementScrollResult = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
9468 expression =
$@"(function() {{
9469 const isDown = '{direction}' === 'down';
9470 const scrollAmount = isDown ? window.innerHeight : -window.innerHeight;
9471 const scrollable = Array.from(document.querySelectorAll('*')).find(el =>
9472 el.scrollHeight > el.clientHeight && getComputedStyle(el).overflowY !== 'hidden'
9474 const initialScrollTop = scrollable.scrollTop;
9475 scrollable.scrollBy({{ left: 0, top: scrollAmount, behavior: 'instant' }});
9476 return {{ initialScrollTop, scrollTop: scrollable.scrollTop, element: scrollable.className || 'body' }};
9478 contextId = CurrentContextId,
9479 returnByValue = true
9480 }, s).ConfigureAwait(
false);
9482 double initialScrollTop = Convert.ToDouble(elementScrollResult?[
"result"]?[
"value"][
"initialScrollTop"]);