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; }
99 private string CurrentRootObjectId {
get;
set; }
109 internal async Task EnablePageEvents()
111 await SendCommand<object>(DevToolsMethods.PageEnable,
new { },
GetEffectiveSessionId()).ConfigureAwait(
false);
114 private ConcurrentDictionary<long, TaskCompletionSource<dynamic>> _responseTasks {
get; } =
new ConcurrentDictionary<long, TaskCompletionSource<dynamic>>();
115 private ConcurrentQueue<(
string Event, dynamic Data)> _events {
get; } =
new ConcurrentQueue<(string, dynamic)>();
119 internal ConcurrentQueue<WindowSession> _windowSessionsQueue {
get;
set; } =
new ConcurrentQueue<WindowSession>();
127 var tabQueue = GetActiveWindowTabQueue();
128 return tabQueue ??
new ConcurrentQueue<KeyValuePair<string, string>>();
136 get => ((PuppeteerClient)PuppeteerClient)._currentTargetId;
138 private int _commandId = 0xdead;
142 internal bool _isRunning {
get;
set; }
146 internal bool _usePipes {
get;
set; }
147 private CancellationTokenSource _cts;
148 private readonly SemaphoreSlim _sendSemaphore =
new SemaphoreSlim(1, 1);
152 internal IPuppeteerClient PuppeteerClient {
get;
set; }
156 internal Browser Browser {
get;
set; }
160 private bool _suppressNetworkEvents {
get;
set; } =
false;
164 internal bool CapturingCalls {
get;
set; } =
false;
165 internal List<GPALCall> CapturedCalls {
get; } =
new List<GPALCall>();
175 return new List<GPALCall>(CapturedCalls);
183 CapturedCalls.Clear();
187 private readonly Dictionary<string, GPALCall> _capturedById =
new Dictionary<string, GPALCall>();
191 internal Task ReceiveTask {
get;
set; }
195 internal TaskCompletionSource<bool> _readerReadyTcs {
get;
set; }
198 List<string> lastErrorMessage =
new List<string>();
201 static readonly
string[] NotRenderedAnswers = {
"Node does not have a layout object",
"Could not compute box model" };
202 bool supressedMessage =
false;
203 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;
233 PuppeteerClient =
new PuppeteerClient();
234 _usePipes = usePipes;
235 _windowSessionsQueue =
new ConcurrentQueue<WindowSession>();
237 _puppeteerUrl = puppeteerUrl;
241 var initTask = InitializeCommunication(Browser);
246 initFinished = initTask.Wait(TimeSpan.FromSeconds(45));
248 catch (AggregateException ex)
251 Exception reason = ex.Flatten().InnerExceptions.FirstOrDefault() ?? ex;
253 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(reason).Throw();
258 if (
false == initFinished)
260 string msg = $
"Puppeteer communication initialization timed out talking to [{puppeteerUrl}]. Workflow canot run without it, Try again.";
262 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg,
this, GPALObjectType.PuppeteerCommunicator);
266 throw new GPALException($
"{GPAL.MyMethodName()}: {msg}");
268 ReceiveTask = initTask.GetAwaiter().GetResult();
274 PuppeteerClient.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 bool notRendered =
true == NotRenderedAnswers.Any(answer =>
true == $
"{message}".StartsWith(answer));
843 string msg =
true == notRendered ?
"Element is not rendered, so it has no position on the page." : $
"CDP error : [{message}].";
844 if (
false == lastErrorMessage.Contains(msg))
846 lastErrorMessage.Add(msg);
847 GPAL.PublishSimpleEvent(
true == notRendered ? GPALEventType.INFO : GPALEventType.ERROR, msg,
this, GPALObjectType.PuppeteerCommunicator);
848 supressedMessage =
false;
850 else if (
false == supressedMessage)
852 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", msg, GPALObjectType.Other);
853 supressedMessage =
true;
856 throw new ApplicationException(msg);
858 else if (message?.error !=
null)
860 if (
true ==
"Session with given id not found.".Equals(message?.error?.message?.ToString()))
864 ClearCurrentSessionId();
865 CurrentFrameSessionId =
null;
871 bool notRendered =
true == NotRenderedAnswers.Any(answer =>
true == $
"{message.error.message}".StartsWith(answer));
872 string msg =
true == notRendered ?
"Element is not rendered, so it has no position on the page." : $
"CDP error : [{message.error.message}].";
873 if (
false == lastErrorMessage.Contains(msg))
875 lastErrorMessage.Add(msg);
876 GPAL.PublishSimpleEvent(
true == notRendered ? GPALEventType.INFO : GPALEventType.ERROR, msg,
this, GPALObjectType.PuppeteerCommunicator);
877 supressedMessage =
false;
879 else if (
false == supressedMessage)
881 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", msg, GPALObjectType.Other);
882 supressedMessage =
true;
885 throw new ApplicationException(msg);
893 if (
null == resultExtractors)
895 var protocolJson = GetProtocolJSON();
896 resultExtractors = CdpResultExtractors.Build(protocolJson);
899 if (resultExtractors !=
null && resultExtractors.TryGetValue(method, out var extractor))
901 if (
null == message || message is
string || message.result ==
null)
903 string msg = $
"Command [{method}] returned null result.";
904 if (
false == lastErrorMessage.Contains(msg))
906 lastErrorMessage.Add(msg);
907 GPAL.PublishSimpleEvent(GPALEventType.WARNING, msg,
this, GPALObjectType.PuppeteerCommunicator);
908 supressedMessage =
false;
910 else if (
false == supressedMessage)
912 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", msg, GPALObjectType.Other);
913 supressedMessage =
true;
917 data = extractor((Newtonsoft.Json.Linq.JObject)message?.result);
921 data = message.result?.ToObject<dynamic>() ??
new { };
934 if (
false == doNotGetSemaphore)
935 _sendSemaphore.Release();
940 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Command [{method}][{parameters}] failed.",
null, GPALObjectType.None, ex);
956 string identifier =
null;
959 if (
true == Browser.UseSelenium)
962 identifier =
BrowserHelper.SeleniumAddScriptToEvaluateOnNewDocument(Browser.BrowserSettings, scriptToInject);
964 else if (
true == Browser.UsePuppeteer)
967 var result = await SendCommand<object>(DevToolsMethods.PageAddScriptToEvaluateOnNewDocument,
new
969 source = scriptToInject
970 }, sessionId ?? ((PuppeteerClient)PuppeteerClient)._currentTargetId).ConfigureAwait(
false);
971 identifier = result?.identifier?.ToString();
974 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Script injected via CDP for sessionId: [{sessionId ?? ((PuppeteerClient)PuppeteerClient)._currentTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
978 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to inject script via CDP",
this, GPALObjectType.Puppeteer, ex);
993 if (
string.IsNullOrEmpty(identifier))
1001 if (
true == Browser.UseSelenium)
1005 else if (
true == Browser.UsePuppeteer)
1007 await SendCommand<object>(DevToolsMethods.PageRemoveScriptToEvaluateOnNewDocument,
new
1010 }, sessionId ?? ((PuppeteerClient)PuppeteerClient)._currentTargetId).ConfigureAwait(
false);
1013 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Injected script [{identifier}] removed via CDP for sessionId: [{sessionId ?? ((PuppeteerClient)PuppeteerClient)._currentTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
1015 catch (Exception ex)
1017 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to remove injected script via CDP",
this, GPALObjectType.Puppeteer, ex);
1022 private readonly List<string> _injectedScriptIdentifiers =
new List<string>();
1028 public async Task<string>
InjectScript(
string scriptToInject,
string sessionId =
null)
1031 if (!
string.IsNullOrEmpty(identifier))
1033 _injectedScriptIdentifiers.Add(identifier);
1043 foreach (var identifier
in _injectedScriptIdentifiers)
1047 _injectedScriptIdentifiers.Clear();
1205 internal async Task ReceivePipeMessages(CancellationToken ct)
1208 _readerReadyTcs?.SetResult(
true);
1211 if (stream ==
null || !stream.IsConnected || !stream.CanRead)
1213 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"OutboundPipe null, disconnected, or not readable",
this, GPALObjectType.PuppeteerCommunicator);
1217 var encoding = Encoding.UTF8;
1220 void ProcessMessageString(
string text)
1224 dynamic message = Newtonsoft.Json.JsonConvert.DeserializeObject(text);
1225 if (message ==
null)
return;
1227 if (message.id !=
null)
1229 if (_responseTasks.TryRemove((
int)message.id, out TaskCompletionSource<dynamic> tcs))
1231 if (message.error !=
null)
1233 string errorMessage = (message.error.message !=
null ? message.error.message.ToString() :
1234 message.error.ToString()) ??
"Unknown CDP error";
1235 if (!errorMessage.Contains(
"wasn't found"))
1237 tcs.TrySetResult(errorMessage);
1241 tcs.TrySetResult(message);
1245 else if (message.method !=
null)
1247 string method = (string)message.method;
1251 if (
true == method.StartsWith(
"Page.") &&
"Page.javascriptDialogOpening" != method)
1254 dynamic paramsData = message.@params;
1255 string title = paramsData?.targetInfo?.title;
1256 string targetType = paramsData?.targetInfo?.type?.ToString();
1257 string sessionId = message.sessionId?.ToString() ?? ((PuppeteerClient)PuppeteerClient)._currentTargetId;
1258 string url = paramsData?.targetInfo?.url?.ToString() ??
"unknown";
1260 if (method.StartsWith(
"Network."))
1262 if (method ==
"Network.requestWillBeSent" ||
1263 method ==
"Network.loadingFinished" ||
1264 method ==
"Network.loadingFailed" ||
1265 method ==
"Network.requestServedFromCache" ||
1266 method ==
"Network.requestWillBeSentExtraInfo")
1268 _events.Enqueue((method, paramsData));
1273 if (
true == CapturingCalls &&
"Network.requestWillBeSent" == method)
1274 RecordCall(paramsData);
1275 else if (
true == CapturingCalls &&
"Network.responseReceived" == method)
1276 RecordStatus(paramsData);
1278 if (_suppressNetworkEvents)
1285 if (
"Page.javascriptDialogOpening" == method &&
null != Browser.BrowserSettings.DialogsAccepted)
1287 bool accept =
true == Browser.BrowserSettings.DialogsAccepted;
1288 string typed = Browser.BrowserSettings.DialogText;
1290 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Answering a [{paramsData?.type}] with accept [{accept}] text [{typed}]",
this, GPALObjectType.PuppeteerCommunicator);
1295 _ = SendCommand<object>(DevToolsMethods.PageHandleJavaScriptDialog,
1296 null == typed ? (
object)
new { accept } :
new { accept, promptText = typed },
1297 sessionId, 3,
true);
1300 if (method ==
"Target.targetCreated" && _sendSemaphore.CurrentCount == 1)
1302 if (
true == targetType?.Equals(
"page") &&
false == url?.StartsWith(
"chrome://") &&
false == url?.Equals(
"about:blank"))
1304 string targetId = paramsData?.targetInfo?.targetId?.ToString();
1305 string browserContextId = paramsData?.targetInfo?.browserContextId?.ToString();
1306 if (!
string.IsNullOrEmpty(targetId))
1310 _ = HandleNewTarget(targetId, browserContextId,
null,
true);
1311 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Detected new uncontrolled tab: TargetId [{targetId}] in Window [{browserContextId}]",
this, GPALObjectType.PuppeteerCommunicator);
1316 GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $
"Received (pipe): [{text}]",
this, GPALObjectType.PuppeteerCommunicator);
1319 catch (Exception ex)
1321 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Error processing pipe message",
this, GPALObjectType.PuppeteerCommunicator, ex);
1327 var pending =
new List<byte>();
1328 var chunk =
new byte[65536];
1332 while (_isRunning && !Browser.BrowserSettings.Process.HasExited && !ct.IsCancellationRequested)
1334 var readTask = stream.ReadAsync(chunk, 0, chunk.Length, ct);
1336 if (await Task.WhenAny(readTask, Task.Delay(5000, ct)).ConfigureAwait(
false) != readTask)
1338 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"No data on OutboundPipe after 5s - Chrome not responding",
this, GPALObjectType.PuppeteerCommunicator);
1342 int bytesRead = readTask.Result;
1347 for (
int i = 0; i < bytesRead; i++)
1351 pending.Add(chunk[i]);
1356 if (0 < pending.Count)
1358 ProcessMessageString(encoding.GetString(pending.ToArray()));
1364 catch (Exception ex)
1366 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"ReceivePipeMessages failed",
this, GPALObjectType.PuppeteerCommunicator, ex);
1380 private async Task ReceiveMessages(CancellationToken ct)
1382 var buffer =
new byte[102400];
1384 while (_isRunning && _webSocket.State == WebSocketState.Open)
1388 if (ct.IsCancellationRequested)
1393 await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure,
"Cancellation requested", CancellationToken.None).ConfigureAwait(
false);
1394 _webSocket.Dispose();
1396 catch (Exception ex)
1398 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Error during WebSocket cleanup on cancellation",
this, GPALObjectType.PuppeteerCommunicator, ex);
1405 using var ms =
new System.IO.MemoryStream();
1406 WebSocketReceiveResult result;
1409 result = await _webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), ct).ConfigureAwait(
false);
1410 ms.Write(buffer, 0, result.Count);
1411 }
while (!result.EndOfMessage);
1415 if (result.MessageType == WebSocketMessageType.Close)
1417 await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure,
"WebSocket closed", CancellationToken.None).ConfigureAwait(
false);
1421 catch (OperationCanceledException)
1425 catch (WebSocketException ex)
1427 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"WebSocket error",
this, GPALObjectType.PuppeteerCommunicator, ex);
1431 if (_webSocket.State == WebSocketState.Closed || _webSocket.State == WebSocketState.Aborted)
1433 try { await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure,
"Cleanup", CancellationToken.None).ConfigureAwait(
false); }
catch { }
1434 _webSocket.Dispose();
1441 var json = Encoding.UTF8.GetString(ms.ToArray());
1442 var message = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);
1444 if (message?.
id !=
null)
1446 if (_responseTasks.TryRemove((
int)message.id, out TaskCompletionSource<dynamic> tcs))
1448 if (message.error !=
null)
1450 string errorMessage = (message.error.message !=
null ? message.error.message.ToString() :
1451 message.error.ToString()) ??
"Unknown CDP error";
1452 if (
false == errorMessage.Contains(
"wasn't found"))
1453 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, errorMessage,
this, GPALObjectType.PuppeteerCommunicator);
1454 tcs.SetResult(errorMessage);
1457 tcs.SetResult(message);
1460 else if (message.method !=
null)
1462 string method = (string)message.method;
1468 if (
true == method.StartsWith(
"Page.") &&
"Page.javascriptDialogOpening" != method)
1471 dynamic paramsData = message.@params;
1472 string title = paramsData.targetInfo?.title;
1473 string targetType = paramsData.targetInfo?.type?.ToString();
1474 string sessionId = message.sessionId?.ToString() ?? ((PuppeteerClient)PuppeteerClient)._currentTargetId;
1475 string url = paramsData.targetInfo?.url?.ToString() ??
"unknown";
1479 if (method.StartsWith(
"Network."))
1482 if (method ==
"Network.requestWillBeSent" ||
1483 method ==
"Network.loadingFinished" ||
1484 method ==
"Network.loadingFailed" ||
1485 method ==
"Network.requestServedFromCache" ||
1486 method ==
"Network.requestWillBeSentExtraInfo")
1488 _events.Enqueue((method, paramsData));
1493 if (
true == CapturingCalls &&
"Network.requestWillBeSent" == method)
1494 RecordCall(paramsData);
1495 else if (
true == CapturingCalls &&
"Network.responseReceived" == method)
1496 RecordStatus(paramsData);
1498 if (method ==
"Network.responseReceived")
1501 string type = paramsData.type?.ToString();
1504 if (type ==
"Document")
1506 this.ServerResponseCode = (int)paramsData.response.status;
1507 string responseUrl = paramsData.response.url?.ToString();
1517 if (_suppressNetworkEvents)
1521 if (method.StartsWith(
"Cast."))
1525 JObject payload = paramsData;
1526 JToken sink = payload[
"sinks"];
1530 CastDevices.Add(
new CastDevice(sink[0][
"name"]?.ToString(), sink[0][
"id"]?.ToString()));
1533 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
1534 $
"[{method}] [{Newtonsoft.Json.JsonConvert.SerializeObject(paramsData)}]",
1535 this, GPALObjectType.PuppeteerCommunicator);
1540 catch (Exception ex)
1542 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
1543 $
"Error processing [{method}]",
1544 this, GPALObjectType.PuppeteerCommunicator, ex);
1551 if (
"Page.javascriptDialogOpening" == method &&
null != Browser.BrowserSettings.DialogsAccepted)
1553 bool accept =
true == Browser.BrowserSettings.DialogsAccepted;
1554 string typed = Browser.BrowserSettings.DialogText;
1556 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Answering a [{paramsData?.type}] with accept [{accept}] text [{typed}]",
this, GPALObjectType.PuppeteerCommunicator);
1561 _ = SendCommand<object>(DevToolsMethods.PageHandleJavaScriptDialog,
1562 null == typed ? (
object)
new { accept } :
new { accept, promptText = typed },
1563 sessionId, 3,
true);
1566 if (method ==
"Target.targetCreated" && _sendSemaphore.CurrentCount == 1)
1568 if (
true == targetType.Equals(
"page") &&
false == url?.StartsWith(
"chrome://") &&
false == url?.Equals(
"about:blank"))
1570 string targetId = paramsData.targetInfo?.targetId?.ToString();
1571 string browserContextId = paramsData.targetInfo?.browserContextId?.ToString();
1573 if (!
string.IsNullOrEmpty(targetId))
1577 await HandleNewTarget(targetId, browserContextId,
null,
true).ConfigureAwait(
false);
1578 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Detected new uncontrolled tab: TargetId [{targetId}] in Window [{browserContextId}]",
this, GPALObjectType.PuppeteerCommunicator);
1612 GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $
"Received: [{Newtonsoft.Json.JsonConvert.SerializeObject(message)}]",
this, GPALObjectType.PuppeteerCommunicator);
1616 catch (Exception ex)
1618 string innerMsg =
"";
1621 if (ex.InnerException !=
null)
1623 innerMsg = $
" Inner: [{ex.InnerException.Message}]";
1628 string exMsg = $
"Error receiving WebSocket message: [{ex.Message}][{innerMsg}]";
1633 GPAL.PublishSimpleEvent(
1634 GPALEventType.DEEPDEBUG,
1637 GPALObjectType.Puppeteer,
1654 private async Task HandleNewTarget(
string targetId,
string browserContextId,
string newSessionId =
null,
bool doNotGetSendSemaphore =
false,
bool? isActive =
null)
1659 bool activeTab = isActive ?? !(
null == newSessionId);
1662 await AddTabToQueue(targetId, newSessionId, activeTab, doNotGetSendSemaphore).ConfigureAwait(
false);
1669 if (
true == CapturingCalls)
1670 await
CaptureCalls(
true, newSessionId).ConfigureAwait(
false);
1672 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Handled new target: ID [{targetId}] in Window [{browserContextId}]",
this, GPALObjectType.PuppeteerCommunicator);
1689 var start = DateTime.UtcNow;
1690 while (DateTime.UtcNow - start < timeout)
1692 if (_events.TryDequeue(out var evt) && evt.Event == eventName)
1694 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Waiting for [{eventName}] got [{evt.Event}]",
this, GPALObjectType.PuppeteerCommunicator);
1695 await Task.Delay(100).ConfigureAwait(
false);
1697 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Event [{eventName}] not received within [{timeout.TotalSeconds}]s",
this, GPALObjectType.PuppeteerCommunicator);
1721 var stopwatch = System.Diagnostics.Stopwatch.StartNew();
1724 while (stopwatch.ElapsedMilliseconds < maxWaitMs)
1727 var box = await SendCommand<dynamic>(
1728 DevToolsMethods.DOMGetBoxModel,
1729 new { backendNodeId = element.ElementBackendNodeId },
1731 ).ConfigureAwait(
false);
1733 if (box?.model?.content !=
null)
1735 var quad = box.model.content;
1736 if (quad.Count >= 8 && quad[2] > quad[0] && quad[7] > quad[1])
1741 else if (
null == box)
1743 List<GPALElement> elems = await
EvaluateSelector(element.
Css, sessionId).ConfigureAwait(
false);
1744 if (0 < elems.Count)
1746 element.ElementBackendNodeId = elems[0].ElementBackendNodeId;
1747 element.ElementHandle = elems[0].ElementHandle;
1748 element.ElementNodeId = elems[0].ElementNodeId;
1749 element.Css = elems[0].Css;
1750 element.Xpath = elems[0].Xpath;
1755 var jsResult = await SendCommand<dynamic>(
1756 DevToolsMethods.RuntimeEvaluate,
1761 const el = document.querySelector('{element.Css}') ||
1762 document.evaluate('{element.Xpath}', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
1763 if (!el) return false;
1764 const r = el.getBoundingClientRect();
1765 return r.width > 0.1 && r.height > 0.1 && r.top >= 0; // visible-ish
1767 contextId = CurrentContextId,
1768 returnByValue = true
1771 ).ConfigureAwait(
false);
1773 if (jsResult?.result?.value is
bool visible && visible)
1778 await Task.Delay(pollIntervalMs).ConfigureAwait(
false);
1782 $
"Timeout waiting for layout on [{element.TagName}][{element.Css ?? element.Xpath}] after [{maxWaitMs}]ms",
1783 this, GPALObjectType.PuppeteerCommunicator);
1803 public async Task<string>
CreateTarget(
string url,
bool newWindow =
false,
string sessionId =
null)
1806 url =
MagicHelper.GetFullUrl(url, Browser, out Browser._areRobotsAllowed);
1808 if (
false == Browser.AreRobotsAllowed &&
true == Browser.BrowserSettings.ObeyRobotsTxt)
1810 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);
1811 url =
"https://google.com";
1814 object parameters =
new { url };
1817 var context = await SendCommand<object>(DevToolsMethods.TargetCreateBrowserContext,
new { }, sessionId).ConfigureAwait(
false);
1818 string browserContextId = context.browserContextId.ToString();
1819 parameters =
new { url, browserContextId };
1822 var result = await SendCommand<object>(DevToolsMethods.TargetCreateTarget, parameters,
null).ConfigureAwait(
false);
1823 string targetId = result.targetId.ToString();
1824 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId },
null).ConfigureAwait(
false);
1825 var session = await SendCommand<object>(DevToolsMethods.TargetAttachToTarget,
new { targetId, flatten = true },
null).ConfigureAwait(
false);
1826 string newSessionId = session.sessionId.ToString();
1827 await AddTabToQueue(targetId, newSessionId).ConfigureAwait(
false);
1839 internal async Task CloseOutputWebSocketAsync()
1844 if (
null != _webSocket)
1845 await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure,
"Client closing", CancellationToken.None).ConfigureAwait(
false);
1858 internal async Task<string> GetInitialTargetId(
string sessionId)
1860 dynamic targets = await SendCommand<dynamic>(DevToolsMethods.TargetGetTargets,
new { },
null).ConfigureAwait(
false);
1861 var pageTarget = ((IEnumerable<dynamic>)targets.targetInfos).FirstOrDefault(t => (
string)t.type ==
"page");
1862 return pageTarget !=
null ? (string)pageTarget.targetId : null;
1870 internal async Task<int> TabCount()
1872 dynamic targets = await SendCommand<dynamic>(DevToolsMethods.TargetGetTargets,
new { },
null).ConfigureAwait(
false);
1873 return ((IEnumerable<dynamic>)targets.targetInfos).Count(t => (
string)t.type ==
"page");
1890 if (
string.IsNullOrWhiteSpace(url))
1892 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"URL is empty or null in GetTargetWindowIdByUrl",
this, GPALObjectType.PuppeteerCommunicator);
1896 url =
MagicHelper.GetFullUrl(url, Browser, out
bool _);
1897 var windowArray = _windowSessionsQueue.ToArray();
1898 foreach (var window
in windowArray)
1900 var tabArray = window.TabQueue.ToArray();
1901 foreach (var tab
in tabArray)
1903 var targetInfo = await SendCommand<object>(DevToolsMethods.TargetGetTargetInfo,
new { targetId = tab.Key },
null).ConfigureAwait(
false);
1904 string targetUrl = targetInfo?.targetInfo?.url?.ToString();
1905 if (targetUrl !=
null && targetUrl.Contains(url))
1907 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Found window with browserContextId [{window.BrowserContextId}] for URL [{url}]",
this, GPALObjectType.PuppeteerCommunicator);
1908 return window.BrowserContextId;
1913 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No window found with a tab matching URL [{url}]",
this, GPALObjectType.PuppeteerCommunicator);
1933 if (
string.IsNullOrWhiteSpace(url))
1935 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"URL is empty or null in GetTargetTabIdByUrl",
this, GPALObjectType.PuppeteerCommunicator);
1939 url =
MagicHelper.GetFullUrl(url, Browser, out
bool _);
1940 var windowArray = _windowSessionsQueue.ToArray();
1942 if (_activeWindowIndex >= 0 && _activeWindowIndex < windowArray.Length)
1944 var tabArray = windowArray[_activeWindowIndex].TabQueue.ToArray();
1945 foreach (var tab
in tabArray)
1947 var targetInfo = await SendCommand<object>(DevToolsMethods.TargetGetTargetInfo,
new { targetId = tab.Key },
null).ConfigureAwait(
false);
1948 string targetUrl = targetInfo?.targetInfo?.url?.ToString();
1949 if (targetUrl !=
null)
1951 string decodedTarget = Uri.UnescapeDataString(targetUrl);
1952 string decodedUrl = Uri.UnescapeDataString(url);
1954 if (
true == decodedTarget.Contains(decodedUrl))
1957 $
"Found tab with targetId [{tab.Key}] for URL [{url}] in active window",
1958 this, GPALObjectType.PuppeteerCommunicator);
1966 for (
int i = 0; i < windowArray.Length; i++)
1968 if (i == _activeWindowIndex)
continue;
1969 var tabArray = windowArray[i].TabQueue.ToArray();
1970 foreach (var tab
in tabArray)
1972 var targetInfo = await SendCommand<object>(DevToolsMethods.TargetGetTargetInfo,
new { targetId = tab.Key },
null).ConfigureAwait(
false);
1973 string targetUrl = targetInfo?.targetInfo?.url?.ToString();
1974 if (targetUrl !=
null && targetUrl.Contains(url))
1976 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Found tab with targetId [{tab.Key}] for URL [{url}] in window [{i}]",
this, GPALObjectType.PuppeteerCommunicator);
1982 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No tab found with URL [{url}]",
this, GPALObjectType.PuppeteerCommunicator);
1992 internal async Task<string> GetNextWindowId(
string id,
string sessionId =
null)
1995 var targets = await SendCommand<object>(DevToolsMethods.TargetGetTargets,
new { },
null).ConfigureAwait(
false);
1996 var windowList = ((IEnumerable<dynamic>)targets.targetInfos)
1997 .Where(t => t.type ==
"browser" || t.type ==
"page")
1998 .GroupBy(t => t.browserContextId ?? t.targetId)
1999 .Select(g => g.First())
2000 .OrderBy(t => t.targetId)
2002 for (
int i = 0; i < windowList.Count; i++)
2004 if (windowList[i].targetId.ToString() ==
id)
2006 return i < windowList.Count - 1 ? windowList[i + 1].targetId.ToString() :
null;
2009 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No next window found for ID: [{id}]",
this, GPALObjectType.PuppeteerCommunicator);
2019 internal async Task<string> GetPreviousWindowId(
string id,
string sessionId =
null)
2022 var targets = await SendCommand<object>(DevToolsMethods.TargetGetTargets,
new { },
null).ConfigureAwait(
false);
2023 var windowList = ((IEnumerable<dynamic>)targets.targetInfos)
2024 .Where(t => t.type ==
"browser" || t.type ==
"page")
2025 .GroupBy(t => t.browserContextId ?? t.targetId)
2026 .Select(g => g.First())
2027 .OrderBy(t => t.targetId)
2029 for (
int i = 0; i < windowList.Count; i++)
2031 if (windowList[i].targetId.ToString() ==
id)
2033 return i > 0 ? windowList[i - 1].targetId.ToString() :
null;
2036 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No previous window found for ID: [{id}]",
this, GPALObjectType.PuppeteerCommunicator);
2045 public async Task<int>
QueryByCss(
string selector,
string sessionId =
null)
2050 var doc = await SendCommand<object>(DevToolsMethods.DOMGetDocument,
new { depth = 0 }, sessionId).ConfigureAwait(
false);
2051 var result = await SendCommand<object>(DevToolsMethods.DOMQuerySelector,
new { nodeId = (int)doc.root.nodeId, selector }, sessionId).ConfigureAwait(
false);
2053 var nodeId = (int)result.nodeId;
2056 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No nodes found for Css: [{selector}]",
this, GPALObjectType.PuppeteerCommunicator);
2065 public string ObjectId {
get;
set; }
2073 public int NodeId {
get;
set; }
2076 private readonly SemaphoreSlim _evaluateSelectorSemaphore =
new SemaphoreSlim(1, 1);
2089 CurrentRootObjectId =
null;
2092 var elements = await
EvaluateSelector(frameSelector, isRecursive:
true).ConfigureAwait(
false);
2093 if (elements.Count == 0)
2095 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG,
"Frame not found: " + frameSelector,
this, GPALObjectType.PuppeteerCommunicator);
2099 var iframeElement = elements[0];
2101 var describe = await SendCommand<dynamic>(DevToolsMethods.DOMDescribeNode,
new
2103 objectId = iframeElement.ElementHandle,
2107 string frameId = describe?.node?.frameId?.ToString();
2109 if (
string.IsNullOrEmpty(frameId))
2111 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"No frameId for frame [" + frameSelector +
"]",
this, GPALObjectType.PuppeteerCommunicator);
2115 CurrentFrameId = frameId;
2119 CurrentFrameSessionId =
null;
2124 var attach = await SendCommand<dynamic>(DevToolsMethods.TargetAttachToTarget,
new
2128 },
null).ConfigureAwait(
false);
2130 string frameSessionId = attach?.sessionId?.ToString();
2131 if (!
string.IsNullOrEmpty(frameSessionId))
2132 CurrentFrameSessionId = frameSessionId;
2140 await Task.Delay(300).ConfigureAwait(
false);
2142 var isolated = await SendCommand<dynamic>(DevToolsMethods.PageCreateIsolatedWorld,
new
2145 worldName =
"gpal_isolated_" + Guid.NewGuid().ToString(
"N"),
2146 grantUniversalAccess = false
2151 CurrentContextId = isolated?.executionContextId?.ToObject<
long?>();
2153 if (CurrentContextId.HasValue)
2156 "Resolved iframe [" + frameSelector +
"] | FrameId: " + frameId +
" | ContextId: " + CurrentContextId.Value,
2157 this, GPALObjectType.PuppeteerCommunicator);
2161 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to create isolated world for " + frameSelector,
this, GPALObjectType.PuppeteerCommunicator);
2181 var found = await
EvaluateSelector(elementSelector, isRecursive:
true).ConfigureAwait(
false);
2183 if (0 == found.Count)
2185 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG,
"Element to search inside not found: " + elementSelector,
this, GPALObjectType.PuppeteerCommunicator);
2191 CurrentRootObjectId = found[0].ElementHandle;
2194 "Searching inside [" + elementSelector +
"] | ObjectId: " + CurrentRootObjectId,
2195 this, GPALObjectType.PuppeteerCommunicator);
2198 public async Task SwitchToShadowDom(
string shadowHostSelector)
2200 var hosts = await
EvaluateSelector(shadowHostSelector, isRecursive:
true).ConfigureAwait(
false);
2201 if (hosts.Count == 0)
2203 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG,
"Shadow host not found: " + shadowHostSelector,
this, GPALObjectType.PuppeteerCommunicator);
2207 var host = hosts[0];
2210 var describe = await SendCommand<dynamic>(DevToolsMethods.DOMDescribeNode,
new
2212 objectId = host.ElementHandle,
2215 }, s).ConfigureAwait(
false);
2217 var shadowRoots = describe?.node?.shadowRoots as IEnumerable<dynamic>;
2219 if (shadowRoots?.Any() !=
true)
2221 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG,
"No shadow root on host: " + shadowHostSelector,
this, GPALObjectType.PuppeteerCommunicator);
2225 var shadowRoot = shadowRoots.First();
2227 if (shadowRoot.backendNodeId !=
null)
2229 var resolve = await SendCommand<dynamic>(DevToolsMethods.DOMResolveNode,
new
2231 backendNodeId = (long)shadowRoot.backendNodeId
2232 }, s).ConfigureAwait(
false);
2234 if (resolve?.@
object?.objectId !=
null)
2236 CurrentRootObjectId = (string)resolve.@
object.objectId;
2240 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
2241 "Resolved shadow DOM [" + shadowHostSelector +
"] | ObjectId: " + CurrentRootObjectId,
2242 this, GPALObjectType.PuppeteerCommunicator);
2256 public async Task<List<GPALElement>>
EvaluateSelector(
string selector,
string sessionId =
null,
bool isRecursive =
false)
2258 var elements =
new List<GPALElement>();
2263 string effectiveSessionId = (!
string.IsNullOrEmpty(sessionId) && sessionId != GetCurrentSessionId())
2267 if (
false == isRecursive)
2268 await _evaluateSelectorSemaphore.WaitAsync().ConfigureAwait(
false);
2272 var nodes =
new List<Node>();
2274 bool inRootContext = !
string.IsNullOrEmpty(CurrentRootObjectId);
2278 bool useIsolatedContext = CurrentContextId.HasValue;
2283 "Evaluating inside the scope set by InShadowDom or InElement, for selector [" + selector +
"]",
2284 this, GPALObjectType.PuppeteerCommunicator);
2290 bool isNodeId =
int.TryParse(selector, out
int tmpNodeId);
2291 bool isObjectId = System.Text.RegularExpressions.Regex.IsMatch(selector,
@"^-?\d+\.\d+\.\d+$");
2294 await SendCommand<object>(DevToolsMethods.DOMGetDocument,
new
2298 }, effectiveSessionId).ConfigureAwait(
false);
2301 if (inRootContext && !isNodeId && !isObjectId)
2305 var shadowQueryResult = await SendCommand<dynamic>(DevToolsMethods.RuntimeCallFunctionOn,
new
2307 objectId = CurrentRootObjectId,
2308 functionDeclaration =
@"
2309 function(selector) {
2312 const cssNodes = this.querySelectorAll(selector);
2313 if (cssNodes?.length > 0) {
2314 nodes = Array.from(cssNodes);
2318 if (nodes.length === 0) {
2320 const result = document.evaluate(selector, this, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
2321 for (let i = 0; i < result.snapshotLength; i++) {
2322 const node = result.snapshotItem(i);
2323 if (node?.getBoundingClientRect) {
2324 node.getBoundingClientRect();
2330 return { nodes: nodes };
2332 arguments = new[] { new { value = selector } },
2333 contextId = CurrentContextId,
2334 returnByValue =
false
2335 }, effectiveSessionId).ConfigureAwait(
false);
2337 if (shadowQueryResult?.result?.objectId !=
null)
2340 var wrapper = await SendCommand<dynamic>(DevToolsMethods.RuntimeGetProperties,
2342 objectId = (string)shadowQueryResult.result.objectId,
2343 contextId = CurrentContextId,
2344 ownProperties = true
2345 }, effectiveSessionId).ConfigureAwait(
false);
2347 string nodesArrayId =
null;
2350 if (wrapper?.result !=
null)
2352 foreach (var p
in wrapper.result)
2354 if (p.name ==
"nodes" && p.value?.objectId !=
null)
2356 nodesArrayId = (string)p.value.objectId;
2362 if (!
string.IsNullOrEmpty(nodesArrayId))
2365 var arrayProps = await SendCommand<dynamic>(DevToolsMethods.RuntimeGetProperties,
2367 objectId = nodesArrayId,
2368 contextId = CurrentContextId,
2369 ownProperties = true
2370 }, effectiveSessionId).ConfigureAwait(
false);
2374 if (arrayProps?.result !=
null)
2376 foreach (var item
in arrayProps.result)
2378 var val = item.value;
2380 && val.type ==
"object"
2381 && val.subtype ==
"node"
2382 && val.objectId !=
null)
2384 nodes.Add(
new Node { ObjectId = (string)val.objectId });
2390 if (nodes.Count > 0)
2391 goto ProcessFoundNodes;
2394 catch (Exception ex)
2397 $
"Shadow DOM query failed for [{selector}]. Falling back to main path.",
2398 this, GPALObjectType.PuppeteerCommunicator, ex);
2402 if (isNodeId || isObjectId)
2404 var requestNodeResult = await SendCommand<object>(DevToolsMethods.DOMRequestNode,
new { objectId = selector }, effectiveSessionId).ConfigureAwait(
false);
2405 if (requestNodeResult?.nodeId !=
null)
2407 var nodeId = (int)requestNodeResult.nodeId;
2408 var resolveNodeResult = await SendCommand<object>(DevToolsMethods.DOMResolveNode,
new { nodeId }, effectiveSessionId).ConfigureAwait(
false);
2409 if (resolveNodeResult?.@
object?.objectId !=
null)
2411 var describeNode = await SendCommand<object>(DevToolsMethods.DOMDescribeNode,
new { objectId = (string)resolveNodeResult.@object.objectId, pierce = true }, effectiveSessionId).ConfigureAwait(
false);
2414 ObjectId = (string)resolveNodeResult.@
object.objectId,
2415 BackendNodeId = describeNode?.node?.backendNodeId ?? 0,
2425 function evaluateSelector(selector) {
2427 let type = 'unknown';
2432 const cssNodes = document.querySelectorAll(selector);
2433 if (cssNodes.length > 0) {
2434 nodes = Array.from(cssNodes);
2439 // If CSS failed, try XPath
2440 if (nodes.length === 0) {
2442 const result = document.evaluate(
2446 XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
2450 // Force CDP to wrap each node by accessing a layout property
2451 for (let i = 0; i < result.snapshotLength; i++) {
2452 const node = result.snapshotItem(i);
2453 if (node && node.getBoundingClientRect) {
2454 // This forces CDP to create a RemoteObject with objectId
2455 node.getBoundingClientRect();
2460 if (nodes.length > 0) {
2463 error = 'No elements found for selector [' + selector + ']';
2466 error = 'Invalid XPath or error: ' + e.message;
2470 return { nodes: nodes };
2473 string escapedSelector = selector;
2474 var evalResult = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
new
2476 expression = $
"({jsFunc})(\"{escapedSelector}\")",
2477 contextId = CurrentContextId,
2478 returnByValue = false
2479 }, effectiveSessionId).ConfigureAwait(
false);
2481 if (evalResult?.result?.objectId !=
null)
2483 var nodesProps = await SendCommand<object>(DevToolsMethods.RuntimeGetProperties,
2485 objectId = (string)evalResult.result.objectId,
2486 contextId = CurrentContextId,
2487 ownProperties = true
2488 }, effectiveSessionId).ConfigureAwait(
false);
2489 dynamic errorProp =
null;
2491 if (nodesProps?.result !=
null)
2493 foreach (var p
in nodesProps.result)
2495 if (p.name ==
"error" && p.value !=
null && p.value.value !=
null)
2503 if (errorProp !=
null)
2505 string errMsg = (string)errorProp.value.value ??
"Unknown selector error";
2507 if (
false == isRecursive)
2508 return new List<GPALElement>();
2511 foreach (var prop
in nodesProps.result)
2513 if (prop.name ==
"nodes" && prop.value?.objectId !=
null)
2515 var nodeArray = await SendCommand<object>(DevToolsMethods.RuntimeGetProperties,
2517 objectId = (string)prop.value.objectId,
2518 contextId = CurrentContextId,
2519 ownProperties = true
2520 }, effectiveSessionId).ConfigureAwait(
false);
2521 foreach (var np
in nodeArray.result)
2523 if (np.value !=
null && np.value.type ==
"object" && np.value.subtype ==
"node")
2524 nodes.Add(
new Node { ObjectId = (string)np.value.objectId });
2534 foreach (var node
in nodes)
2538 var attributes =
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
2540 var describeNode = await SendCommand<object>(DevToolsMethods.DOMDescribeNode,
new { objectId = node.ObjectId, pierce = true }, effectiveSessionId).ConfigureAwait(
false);
2541 if (describeNode?.node !=
null)
2543 node.BackendNodeId = describeNode.node.backendNodeId ?? 0;
2544 var requestNodeResult = await SendCommand<object>(DevToolsMethods.DOMRequestNode,
new
2546 objectId = node.ObjectId
2547 }, effectiveSessionId).ConfigureAwait(
false);
2549 node.NodeId = requestNodeResult.nodeId;
2550 attributes[
"tagName"] = describeNode.node.nodeName;
2553 var jsProps = await SendCommand<object>(DevToolsMethods.RuntimeCallFunctionOn,
new
2555 functionDeclaration =
@"
2557 const rect = this.getBoundingClientRect();
2558 const cleanText = (str) => (str || '').replace(/\s+/g, ' ').trim();
2561 enabled: !this.disabled,
2562 // a checkbox says checked and an option says selected, and Selected is
2563 // one question over both of them, the way it is on selenium
2564 selected: this.checked === true || this.selected === true,
2565 indeterminate: this.indeterminate === true,
2566 displayed: this.offsetParent !== null,
2567 href: this.href || '',
2568 src: this.src || '',
2570 value: cleanText(this.value),
2571 text: cleanText(this.text || this.innerText),
2572 placeholder: cleanText(this.placeholder),
2574 index: this.index || '',
2575 length: this.files ? this.files.length : this.length || '',
2576 type: this.type || '',
2579 for (let {name,value} of Array.from(this.attributes||[])) attrs[name]=value;
2581 // Force boolean properties last - multiple not getting set above, being dropped by cdp?
2582 attrs.multiple = this.multiple === true;
2584 function getCss(el){
2585 if(!(el instanceof Element)) return '';
2587 while(el && el.nodeType===1){
2588 let part=el.nodeName.toLowerCase();
2589 if(el.id){part+='#'+el.id; parts.unshift(part); break;}
2591 while((sib=sib.previousElementSibling)!=null){if(sib.nodeName.toLowerCase()===part)nth++;}
2592 if(nth>1) part+=':nth-of-type('+nth+')';
2593 parts.unshift(part); el=el.parentElement;
2595 return parts.join(' > ');
2597 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}};
2599 objectId = node.ObjectId,
2600 contextId = CurrentContextId,
2601 returnByValue = true
2602 }, effectiveSessionId).ConfigureAwait(
false);
2604 if (jsProps?.result?.value !=
null)
2606 var dict = ((Newtonsoft.Json.Linq.JObject)jsProps.result.value).ToObject<Dictionary<string, object>>();
2608 if (dict.ContainsKey(
"attributes"))
2610 var jsAttrDict = ((Newtonsoft.Json.Linq.JObject)dict[
"attributes"]).ToObject<Dictionary<string, object>>();
2611 foreach (var kvp
in jsAttrDict)
if (!attributes.ContainsKey(kvp.Key)) attributes[kvp.Key] = kvp.Value;
2614 if (dict.ContainsKey(
"boundingRect"))
2616 var brObj = ((Newtonsoft.Json.Linq.JObject)dict[
"boundingRect"]).ToObject<Dictionary<string, object>>();
2617 float ToFloat(
object o)
2619 if (o is
double d)
return (
float)d;
2620 if (o is
long l)
return (
float)l;
2621 if (o is
int i)
return (
float)i;
2622 if (o is
string s &&
float.TryParse(s, out var r))
return r;
2628 X = ToFloat(brObj.ContainsKey(
"x") ? brObj[
"x"] : 0),
2629 Y = ToFloat(brObj.ContainsKey(
"y") ? brObj[
"y"] : 0),
2630 Width = ToFloat(brObj.ContainsKey(
"width") ? brObj[
"width"] : 0),
2631 Height = ToFloat(brObj.ContainsKey(
"height") ? brObj[
"height"] : 0),
2632 Top = ToFloat(brObj.ContainsKey(
"top") ? brObj[
"top"] : 0),
2633 Left = ToFloat(brObj.ContainsKey(
"left") ? brObj[
"left"] : 0),
2634 Bottom = ToFloat(brObj.ContainsKey(
"bottom") ? brObj[
"bottom"] : 0),
2635 Right = ToFloat(brObj.ContainsKey(
"right") ? brObj[
"right"] : 0)
2638 attributes[
"boundingRect"] = rect;
2639 attributes[
"location"] =
new System.Drawing.Point((
int)rect.X, (
int)rect.Y);
2640 attributes[
"size"] =
new System.Drawing.
Size((
int)rect.Width, (
int)rect.Height);
2643 if (dict.ContainsKey(
"css")) attributes[
"css"] = dict[
"css"]?.ToString();
2646 attributes[
"xpath"] = await
GetXPathForElement(node.BackendNodeId).ConfigureAwait(
false);
2650 ElementHandle = node.ObjectId,
2651 ElementBackendNodeId = node.BackendNodeId,
2652 ElementNodeId = node.NodeId
2683 return new List<GPALElement>();
2688 if (
false == isRecursive)
2689 _evaluateSelectorSemaphore.Release();
2701 public async Task<string>
GetCssForElement(
int backendNodeId,
bool optimized =
false,
string sessionId =
null)
2705 function getCssSelector(node) {
2706// console.log('getCssSelector called with node:', node);
2708 if (!node || node.nodeType !== Node.ELEMENT_NODE) {
2709 console.error('Invalid node, returning empty string');
2714// console.log('Found ID:', node.id);
2715 return '#' + node.id.replace(/([ #.;+*~'!^$[\]()=>|\/@])/g, '\\$1');
2718 let selector = node.localName.toLowerCase();
2719// console.log('Tag name:', selector);
2721 if (node.classList && node.classList.length > 0) {
2722 const classes = Array.from(node.classList)
2723 .filter(c => c && /^[a-zA-Z][\w-]*$/.test(c))
2724 .map(c => c.replace(/([ #.;+*~'!^$[\]()=>|\/@])/g, '\\$1'));
2725 if (classes.length > 0) {
2726 selector += '.' + classes.join('.');
2728 // console.log('Selector with classes:', selector);
2732 const matches = document.querySelectorAll(selector);
2733 // console.log('Matches for selector:', matches.length, selector);
2734 if (matches.length === 1 && matches[0] === node) {
2738 // console.error('Error in querySelectorAll:', e.message);
2742 // console.log('Returning fallback selector:', selector);
2754 public async Task<string>
GetXPathForElement(
int backendNodeId,
bool optimized =
true,
string sessionId =
null)
2759 function getXPathForElement(node, optimized) {
2760 let Elements = { DOMPath: {} };
2762 Elements.DOMPath.xPath = function(node, optimized) {
2763 if (node.nodeType === Node.DOCUMENT_NODE) return '/';
2765 let contextNode = node;
2766 while (contextNode) {
2767 const step = Elements.DOMPath._xPathValue(contextNode, optimized);
2770 if (step.optimized) break;
2771 contextNode = contextNode.parentNode;
2774 return (steps.length && steps[0].optimized ? '' : '/') + steps.map(step => step.value).join('/');
2777 Elements.DOMPath._xPathValue = function(node, optimized) {
2779 const ownIndex = Elements.DOMPath._xPathIndex(node);
2780 if (ownIndex === -1) return null;
2781 switch (node.nodeType) {
2782 case Node.ELEMENT_NODE:
2783 if (optimized && node.getAttribute('id'))
2784 return new Elements.DOMPath.Step('//*[@id=\'' + node.getAttribute('id') + '\']', true);
2785 ownValue = node.localName;
2787 case Node.ATTRIBUTE_NODE:
2788 ownValue = '@' + node.nodeName;
2790 case Node.TEXT_NODE:
2791 case Node.CDATA_SECTION_NODE:
2792 ownValue = 'text()';
2794 case Node.PROCESSING_INSTRUCTION_NODE:
2795 ownValue = 'processing-instruction()';
2797 case Node.COMMENT_NODE:
2798 ownValue = 'comment()';
2800 case Node.DOCUMENT_NODE:
2808 ownValue += '[' + ownIndex + ']';
2809 return new Elements.DOMPath.Step(ownValue, node.nodeType === Node.DOCUMENT_NODE);
2812 Elements.DOMPath._xPathIndex = function(node) {
2813 function areNodesSimilar(left, right) {
2814 if (left === right) return true;
2815 if (left.nodeType === Node.ELEMENT_NODE && right.nodeType === Node.ELEMENT_NODE)
2816 return left.localName === right.localName;
2817 if (left.nodeType === right.nodeType) return true;
2818 const leftType = left.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left.nodeType;
2819 const rightType = right.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right.nodeType;
2820 return leftType === rightType;
2822 const siblings = node.parentNode ? node.parentNode.children : null;
2823 if (!siblings) return 0;
2824 let hasSameNamedElements;
2825 for (let i = 0; i < siblings.length; ++i) {
2826 if (areNodesSimilar(node, siblings[i]) && siblings[i] !== node) {
2827 hasSameNamedElements = true;
2831 if (!hasSameNamedElements) return 0;
2833 for (let i = 0; i < siblings.length; ++i) {
2834 if (areNodesSimilar(node, siblings[i])) {
2835 if (siblings[i] === node) return ownIndex;
2842 Elements.DOMPath.Step = function(value, optimized) {
2844 this.optimized = optimized || false;
2847 return Elements.DOMPath.xPath(node, optimized);
2861 public async Task<string>
GetXPathOrCssForElement(
int backendNodeId,
string jsFunc,
bool optimized,
string sessionId)
2866 var resolve = await SendCommand<dynamic>(DevToolsMethods.DOMResolveNode,
new { backendNodeId, pierce = true }, sessionId).ConfigureAwait(
false);
2867 string objectId = resolve.@
object.objectId;
2870 var result = await SendCommand<dynamic>(DevToolsMethods.RuntimeCallFunctionOn,
new
2873 functionDeclaration = $
"function(optimized) {{ return ({jsFunc})(this, optimized); }}",
2874 arguments = new object[] { new { value = optimized } },
2875 contextId = CurrentContextId,
2876 returnByValue =
true
2877 }, sessionId).ConfigureAwait(
false);
2882 return result?.result?.value?.ToString() ??
"";
2884 catch (Exception ex)
2887 $
"Failed to get XPath for backendNodeId [{backendNodeId}]",
2888 this, GPALObjectType.PuppeteerCommunicator, ex);
2894 static Dictionary<string, string> GetAttributesAsDictionary(
string jsonString)
2898 using System.Text.Json.JsonDocument doc = System.Text.Json.JsonDocument.Parse(jsonString);
2901 if (!doc.RootElement.TryGetProperty(
"node", out System.Text.Json.JsonElement nodeElement))
2907 if (!nodeElement.TryGetProperty(
"attributes", out System.Text.Json.JsonElement attributesElement) ||
2908 attributesElement.ValueKind != System.Text.Json.JsonValueKind.Array)
2913 var dict =
new Dictionary<string, string>();
2914 for (
int i = 0; i < attributesElement.GetArrayLength() - 1; i += 2)
2916 string key = attributesElement[i].GetString();
2917 string value = attributesElement[i + 1].GetString();
2923 catch (System.Text.Json.JsonException)
2933 public async Task LeftClickByCss(
string selector,
string sessionId =
null)
2935 List<GPALElement> elems = await EvaluateSelector(selector, sessionId).ConfigureAwait(
false);
2936 List<dynamic> responses =
new List<dynamic>();
2939 await ClickElement(elem, sessionId, responses, ClickType.LeftClick).ConfigureAwait(
false);
2948 public async Task<dynamic>
ExecuteJavaScript(
string expression,
string sessionId =
null)
2951 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
2954 contextId = CurrentContextId,
2955 returnByValue = true,
2958 , sessionId).ConfigureAwait(
false);
2972 internal string LastNavigationError {
get;
set; }
2995 public async Task
CaptureCalls(
bool capture,
string sessionId =
null)
2997 CapturingCalls = capture;
3003 if (
true == capture)
3004 foreach (
string session
in KnownSessions(sessionId))
3005 await SendCommand<object>(DevToolsMethods.NetworkEnable,
new { }, session).ConfigureAwait(
false);
3009 private List<string> KnownSessions(
string sessionId)
3011 List<string> retVal =
new List<string>();
3013 foreach (
string session
in new[] { sessionId, GetEffectiveSessionId() })
3014 if (
false ==
string.IsNullOrEmpty(session) &&
false == retVal.Contains(session))
3015 retVal.Add(session);
3017 foreach (WindowSession window
in _windowSessionsQueue.ToArray())
3018 foreach (KeyValuePair<string, string> tab
in window.TabQueue.ToArray())
3019 if (
false ==
string.IsNullOrEmpty(tab.Value) &&
false == retVal.Contains(tab.Value))
3020 retVal.Add(tab.Value);
3026 private void RecordStatus(dynamic paramsData)
3030 string requestId = paramsData?.requestId?.ToString();
3032 if (
false ==
string.IsNullOrEmpty(requestId) &&
true == _capturedById.TryGetValue(requestId, out GPALCall call))
3033 call.Status = (int)(paramsData?.response?.status ?? 0);
3035 catch (Exception ex)
3037 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Could not record a response status",
this, GPALObjectType.PuppeteerCommunicator, ex);
3042 private void RecordCall(dynamic paramsData)
3046 GPALCall call =
new GPALCall
3048 Url = paramsData?.request?.url?.ToString(),
3049 Method = paramsData?.request?.method?.ToString(),
3050 ResourceType = paramsData?.type?.ToString(),
3051 PostData = paramsData?.request?.postData?.ToString(),
3052 Initiator = paramsData?.initiator?.type?.ToString()
3056 if (paramsData?.request?.headers is Newtonsoft.Json.Linq.JObject headers)
3057 foreach (KeyValuePair<string, Newtonsoft.Json.Linq.JToken> header
in headers)
3058 call.Headers[header.Key] = header.Value?.ToString();
3062 string filter = Browser?.BrowserSettings?.CallFilter;
3064 if (
false ==
string.IsNullOrEmpty(filter) &&
true != call.Url?.Contains(filter))
3067 call.RequestId = paramsData?.requestId?.ToString();
3069 if (
false ==
string.IsNullOrEmpty(call.RequestId))
3070 _capturedById[call.RequestId] = call;
3072 CapturedCalls.Add(call);
3074 catch (Exception ex)
3076 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Could not record a request",
this, GPALObjectType.PuppeteerCommunicator, ex);
3080 internal async Task SetExtraHttpHeaders(Dictionary<string, object> headers,
string sessionId =
null)
3082 sessionId ??= GetEffectiveSessionId();
3086 await SendCommand<object>(DevToolsMethods.NetworkEnable,
new { }, sessionId).ConfigureAwait(
false);
3087 await SendCommand<object>(DevToolsMethods.NetworkSetExtraHTTPHeaders,
new { headers }, sessionId).ConfigureAwait(
false);
3090 public async Task<bool> NavigateToUrl(
string url,
string sessionId)
3092 _attachedFilesPerSession.Clear();
3093 var result = await SendCommand<object>(DevToolsMethods.PageNavigate,
new { url }, sessionId).ConfigureAwait(
false);
3097 LastNavigationError = (result as Newtonsoft.Json.Linq.JObject)?[
"errorText"]?.ToString();
3100 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to Go to [{url}]",
this, GPALObjectType.PuppeteerCommunicator);
3101 else if (
false ==
string.IsNullOrEmpty(LastNavigationError))
3102 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to Go to [{url}][{LastNavigationError}]",
this, GPALObjectType.PuppeteerCommunicator);
3105 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Go to [{url}]",
this, GPALObjectType.PuppeteerCommunicator);
3107 return null != result &&
true ==
string.IsNullOrEmpty(LastNavigationError);
3121 public async Task<dynamic>
Back(
string sessionId =
null)
3124 dynamic history = await SendCommand<object>(DevToolsMethods.PageGetNavigationHistory,
new { }, sessionId).ConfigureAwait(
false);
3125 if (history?.currentIndex !=
null)
3127 var entries = (IEnumerable<dynamic>)history.entries;
3128 var nextEntry = entries.ElementAtOrDefault((
int)history.currentIndex - 1);
3129 if (nextEntry?.
id !=
null)
3131 var result = await SendCommand(DevToolsMethods.PageNavigateToHistoryEntry,
new { entryId = (int)nextEntry.id }, sessionId).ConfigureAwait(
false);
3135 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No previous page in history",
this, GPALObjectType.PuppeteerClient);
3148 return await SendCommand<object>(DevToolsMethods.PageCaptureScreenshot,
new { format =
"png" }, sessionId).ConfigureAwait(
false);
3163 public async Task
CastDesktop(
string sinkName,
string sessionId)
3165 CastDevice sink =
null;
3169 await SendCommand<object>(DevToolsMethods.CastDisable,
null, sessionId).ConfigureAwait(
false);
3171 while (
null == sink && 0 < retries--)
3173 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Searching for [{sinkName}].",
this, GPALObjectType.PuppeteerCommunicator);
3174 await SendCommand<object>(DevToolsMethods.CastEnable,
new { }, sessionId).ConfigureAwait(
false);
3176 await Task.Delay(3_000).ConfigureAwait(
false);
3178 sink = CastDevices.Find(device => device.name == sinkName);
3181 await Task.Delay(3_000).ConfigureAwait(
false);
3185 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"[{sinkName}] found.",
this, GPALObjectType.PuppeteerCommunicator);
3186 SinkName = sinkName;
3187 object param =
new { sinkName = sink.name };
3190 await SendCommand<object>(DevToolsMethods.CastStartDesktopMirroring, param, sessionId).ConfigureAwait(
false);
3193 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"[{sinkName}] NOT found.",
this, GPALObjectType.PuppeteerCommunicator);
3208 public async Task
CastTab(
string sinkName,
string sessionId)
3210 CastDevice sink =
null;
3214 await SendCommand<object>(DevToolsMethods.CastDisable,
null, sessionId).ConfigureAwait(
false);
3216 while (
null == sink && 0 < retries--)
3218 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Searching for [{sinkName}].",
this, GPALObjectType.PuppeteerCommunicator);
3219 await SendCommand<object>(DevToolsMethods.CastEnable,
new { }, sessionId).ConfigureAwait(
false);
3221 await Task.Delay(3_000).ConfigureAwait(
false);
3223 sink = CastDevices.Find(device => device.name == sinkName);
3226 await Task.Delay(3_000).ConfigureAwait(
false);
3230 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"[{sinkName}] found.",
this, GPALObjectType.PuppeteerCommunicator);
3231 SinkName = sinkName;
3232 object param =
new { sinkName = sink.name };
3235 await SendCommand<object>(DevToolsMethods.CastStartTabMirroring, param, sessionId).ConfigureAwait(
false);
3238 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"[{sinkName}] NOT found.",
this, GPALObjectType.PuppeteerCommunicator);
3253 public async Task<bool>
StopCasting(
string sessionId)
3258 await SendCommand<object>(DevToolsMethods.CastStopCasting,
new { sinkName = SinkName }, sessionId).ConfigureAwait(
false);
3279 public async Task<bool>
CheckNetworkIdle(
string sessionId =
null,
int maxConnections = 0,
int timeoutMs = 30000,
int pruneMs = 3000,
string sessionToken =
null)
3281 bool retVal =
false;
3282 GPALEventType consoleTypeSave =
GPAL.GPALSettings.ConsoleEvents;
3283 GPALEventType debugTypeSave =
GPAL.GPALSettings.DebugEvents;
3284 string currentErrorMessage =
null;
3287 if (lastSessionToken != sessionToken)
3289 lastErrorMessage.Clear();
3290 supressedMessage =
false;
3291 lastSessionToken = sessionToken;
3294 currentErrorMessage = $
"Waiting up to [{timeoutMs}] ms for network to idle to [{maxConnections}] connections for [{pruneMs}] ms";
3296 GPAL.
PublishSimpleEvent(GPALEventType.INFO, currentErrorMessage,
this, GPALObjectType.PuppeteerCommunicator);
3300 _suppressNetworkEvents =
true;
3301 await SendCommand(DevToolsMethods.NetworkEnable,
new { cacheDisabled = true }, sessionId).ConfigureAwait(
false);
3302 var start = DateTime.UtcNow;
3303 DateTime? lastRequestTime =
null;
3304 var pendingEvents =
new List<(string Event, dynamic Data, DateTime Timestamp)>();
3305 int unmatchedCount = 0;
3306 bool isFirstIteration =
true;
3310 await Task.Delay(500).ConfigureAwait(
false);
3313 while (DateTime.UtcNow - start < TimeSpan.FromMilliseconds(timeoutMs))
3316 while (_events.TryDequeue(out (
string Event, dynamic Data) evt))
3318 pendingEvents.Add((evt.Event, evt.Data, DateTime.UtcNow));
3322 pendingEvents.Sort((a, b) => a.Timestamp.CompareTo(b.Timestamp));
3325 foreach (var evt
in pendingEvents)
3327 string requestId = evt.Data?.requestId?.ToString() ??
"unknown";
3328 string url = evt.Data?.url?.ToString() ??
"unknown";
3329 string resourceType = evt.Data?.resourceType?.ToString() ??
"unknown";
3330 string initiator = evt.Data?.initiator?.type?.ToString() ??
"unknown";
3331 string frameId = evt.Data?.frameId?.ToString() ??
"unknown";
3332 string loaderId = evt.Data?.loaderId?.ToString() ??
"unknown";
3333 string dataJson = evt.Data !=
null ? Newtonsoft.Json.JsonConvert.SerializeObject(evt.Data) :
"null";
3335 if (evt.Event ==
"Network.requestWillBeSent")
3337 lastRequestTime = DateTime.UtcNow;
3340 else if (evt.Event ==
"Network.requestWillBeSentExtraInfo")
3342 string associatedRequestId = evt.Data?.associatedRequestId?.ToString();
3345 else if (evt.Event ==
"Network.loadingFinished" || evt.Event ==
"Network.loadingFailed")
3348 if (unmatchedCount % 10 == 0)
3353 else if (evt.Event ==
"Network.requestServedFromCache")
3357 else if (isFirstIteration)
3362 pendingEvents.Clear();
3365 if (lastRequestTime ==
null || (DateTime.UtcNow - lastRequestTime >= TimeSpan.FromMilliseconds(pruneMs)))
3372 if (isFirstIteration)
3374 isFirstIteration =
false;
3377 await Task.Delay(50, _cts.Token).ConfigureAwait(
false);
3383 catch (Exception ex)
3385 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to check network idle",
null, GPALObjectType.PuppeteerCommunicator, ex);
3393 await SendCommand(DevToolsMethods.NetworkDisable,
new { }, sessionId).ConfigureAwait(
false);
3395 _suppressNetworkEvents =
false;
3397 currentErrorMessage = $
"Network IS{(retVal ? "" : " NOT
")} Idle. Status [{retVal}]";
3399 if (
false == lastErrorMessage.Contains(currentErrorMessage))
3401 lastErrorMessage.Add(currentErrorMessage);
3402 GPAL.
PublishSimpleEvent(GPALEventType.INFO, currentErrorMessage,
this, GPALObjectType.PuppeteerCommunicator);
3403 supressedMessage =
false;
3405 else if (
false == supressedMessage)
3407 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Supressing repeat messages", currentErrorMessage, GPALObjectType.Other);
3408 supressedMessage =
true;
3419 public async Task ClearReferrer(
string sessionId =
null)
3421 sessionId ??= GetEffectiveSessionId();
3422 await SendCommand<object>(DevToolsMethods.NetworkSetExtraHTTPHeaders,
new { headers = new { Referer =
"" } }, sessionId).ConfigureAwait(
false);
3430 private void CheckForLastBrowserWindowOpen(
string reason)
3432 if (_windowSessionsQueue.IsEmpty)
3434 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);
3449 public async Task<bool>
CloseTab(
string url =
null,
string tabId =
null,
string sessionId =
null)
3453 return await
CloseTab(targetId, sessionId).ConfigureAwait(
false);
3462 public async Task<bool>
CloseTab(
string targetId,
string sessionId =
null)
3464 bool retVal =
false;
3465 if (
string.IsNullOrEmpty(targetId))
3467 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No targetId provided for CloseTab",
this, GPALObjectType.PuppeteerCommunicator);
3472 await SendCommand<object>(DevToolsMethods.TargetCloseTarget,
new { targetId },
null).ConfigureAwait(
false);
3473 bool removed = RemoveTabFromQueue(targetId);
3476 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Failed to remove tab [{targetId}] from queue",
this, GPALObjectType.PuppeteerCommunicator);
3479 var windowArray = _windowSessionsQueue.ToArray();
3480 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
3482 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"No active window after closing tab [{targetId}]",
this, GPALObjectType.PuppeteerCommunicator);
3487 var tabQueue = windowArray[_activeWindowIndex].TabQueue;
3488 if (tabQueue !=
null && tabQueue.IsEmpty)
3490 string browserContextId = windowArray[_activeWindowIndex].BrowserContextId;
3491 await
CloseWindow(browserContextId, sessionId).ConfigureAwait(
false);
3492 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Closed window for targetId [{targetId}] (context [{browserContextId}]) as it has no tabs",
this, GPALObjectType.PuppeteerCommunicator);
3497 CheckForLastBrowserWindowOpen(
"CloseTab()");
3512 public async Task<bool>
CloseWindow(
string browserContextId,
string sessionId =
null)
3514 bool retVal =
false;
3517 var windowArray = _windowSessionsQueue.ToArray();
3518 var window = windowArray.FirstOrDefault(w => w.BrowserContextId == browserContextId);
3521 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Window with browserContextId [{browserContextId}] not found",
this, GPALObjectType.PuppeteerCommunicator);
3526 var tabArray = window.TabQueue.ToArray();
3527 foreach (var tab
in tabArray)
3529 await SendCommand<object>(DevToolsMethods.TargetCloseTarget,
new { targetId = tab.Key },
null).ConfigureAwait(
false);
3530 RemoveTabFromQueue(tab.Key);
3533 if (RemoveWindowFromQueue(browserContextId))
3535 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Closed window with browserContextId [{browserContextId}]",
this, GPALObjectType.PuppeteerCommunicator);
3540 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to remove window with browserContextId [{browserContextId}]",
this, GPALObjectType.PuppeteerCommunicator);
3544 catch (Exception ex)
3546 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to close window with browserContextId [{browserContextId}]",
this, GPALObjectType.PuppeteerCommunicator, ex);
3549 CheckForLastBrowserWindowOpen(
"CloseWindow()");
3579 string domain =
null,
3580 string storeName =
null,
3585 int deletedCount = 0;
3586 bool retVal =
false;
3588 bool IsMissing(
string s) =>
string.IsNullOrEmpty(s);
3592 string origin =
false ==
string.IsNullOrEmpty(domain) ? UrlHelper.GetOrigin(domain) : UrlHelper.GetOrigin(
BrowserHelper.
GetCurrentUrl(Browser.BrowserSettings));
3594 if (IsMissing(domain))
3595 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"DOMAIN IS MISSING. Deleting all [{storageType}] for origin [{origin}].",
this, GPALObjectType.PuppeteerCommunicator);
3597 switch (storageType)
3601 var cookiesJson = await
GetStorage(
"cookie", sessionId, domain, storeName, path, key).ConfigureAwait(
false);
3602 var cookies = JsonConvert.DeserializeObject<List<dynamic>>(cookiesJson) ??
new List<dynamic>();
3603 foreach (dynamic c
in cookies)
3605 var delParams =
new Dictionary<string, object>
3607 {
"name", (string)c.Key },
3608 {
"domain", (string)c.Domain },
3610 {
"path", (string)c.Path },
3616 JToken cookiePartition = ((JObject)c)[
"PartitionKey"];
3618 if (
null != cookiePartition && JTokenType.Object == cookiePartition.Type)
3620 var pk =
new Dictionary<string, object>
3622 {
"topLevelSite", (string)cookiePartition[
"topLevelSite"] },
3623 {
"hasCrossSiteAncestor", (
bool?)cookiePartition[
"hasCrossSiteAncestor"] }
3626 delParams.Add(
"partitionKey", pk);
3631 dynamic deleteReply = await SendCommand(DevToolsMethods.NetworkDeleteCookies, delParams, sessionId).ConfigureAwait(
false);
3633 retVal =
null != deleteReply;
3640 case "localStorage":
3641 case "sessionStorage":
3643 if (IsMissing(domain))
3645 string js =
$@"(function() {{
3647 {storageType}.clear();
3654 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
3657 awaitPromise = false,
3658 contextId = CurrentContextId,
3659 returnByValue = true
3660 }, sessionId).ConfigureAwait(
false).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3670 var keysJson = await
GetStorage(storageType, sessionId, domain).ConfigureAwait(
false);
3674 List<string> keys =
new List<string>();
3676 if (
false ==
string.IsNullOrWhiteSpace(keysJson))
3678 JToken storeRead = JToken.Parse(keysJson);
3680 if (JTokenType.Object == storeRead.Type)
3681 keys = ((JObject)storeRead).Properties().Select(each => each.Name).ToList();
3682 else if (JTokenType.Array == storeRead.Type)
3683 keys = storeRead.Select(each => each.ToString()).ToList();
3685 foreach (var k
in keys)
3687 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
3689 expression = $
"{storageType}.removeItem('{EscapeJsString(k)}');",
3690 awaitPromise = false,
3691 contextId = CurrentContextId,
3692 returnByValue = true
3693 }, sessionId).ConfigureAwait(
false).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3700 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
3702 expression = $
"{storageType}.removeItem('{EscapeJsString(key)}');",
3703 awaitPromise = false,
3704 contextId = CurrentContextId,
3705 returnByValue = true
3706 }, sessionId).ConfigureAwait(
false).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3714 if (IsMissing(domain))
3716 retVal = SendCommand<dynamic>(DevToolsMethods.StorageClearDataForOrigin,
new
3719 storageTypes =
"indexeddb"
3720 }, sessionId).GetAwaiter().GetResult() !=
null;
3728 if (IsMissing(storeName))
3731 var dbsJson = await
GetStorage(
"indexeddb", sessionId, domain).ConfigureAwait(
false);
3732 var dbs = JsonConvert.DeserializeObject<List<string>>(dbsJson) ??
new List<string>();
3733 foreach (var db
in dbs)
3735 string js =
$@"(function(){{ indexedDB.deleteDatabase('{EscapeJsString(db)}'); }})();";
3736 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
3739 awaitPromise = true,
3740 contextId = CurrentContextId,
3741 returnByValue = true
3742 }, sessionId).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3747 else if (IsMissing(key))
3749 string js =
$@"(function(){{ indexedDB.deleteDatabase('{EscapeJsString(storeName)}'); }})();";
3750 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
3753 awaitPromise = true,
3754 contextId = CurrentContextId,
3755 returnByValue = true
3756 }, sessionId).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3762 string js =
$@"(function() {{
3763 return new Promise((resolve, reject) => {{
3764 const dbName = '{EscapeJsString(path)}'; // path = dbName (per your note)
3765 const store = '{EscapeJsString(storeName)}'; // storeName = store (per your note)
3766 const recordKey = '{EscapeJsString(key)}';
3768 const openReq = indexedDB.open(dbName);
3770 openReq.onerror = () => reject(openReq.error);
3772 openReq.onsuccess = () => {{
3773 const db = openReq.result;
3775 const tx = db.transaction(store, 'readwrite');
3776 const objectStore = tx.objectStore(store);
3778 objectStore.delete(recordKey);
3780 tx.oncomplete = () => {{
3782 resolve(true); // so your .Contains(""true"") check still works
3785 tx.onerror = () => {{
3794 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
3798 awaitPromise = true,
3799 contextId = CurrentContextId,
3800 returnByValue = true
3801 }, sessionId).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3811 if (IsMissing(domain))
3813 string js =
@"(async function() {
3815 const keys = await caches.keys();
3816 await Promise.all(keys.map(k => caches.delete(k)));
3823 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
3826 awaitPromise = true,
3827 contextId = CurrentContextId,
3828 returnByValue = true
3829 }, sessionId).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3842 var cachesJson = await
GetStorage(
"cache", sessionId, domain, storeName).ConfigureAwait(
false);
3843 var cacheEntries = JsonConvert.DeserializeObject<List<string>>(cachesJson) ??
new List<string>();
3845 foreach (var each
in cacheEntries)
3847 string cacheDelete = IsMissing(storeName)
3848 ? $
"caches.delete('{EscapeJsString(each)}');"
3849 : $
"caches.open('{EscapeJsString(storeName)}').then(cache => cache.delete('{EscapeJsString(each)}',{{ignoreVary: true}}));";
3851 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
3853 expression = cacheDelete,
3854 awaitPromise = true,
3855 contextId = CurrentContextId,
3856 returnByValue = true
3857 }, sessionId).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3864 retVal = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
3866 expression = $
"caches.open('{EscapeJsString(storeName)}').then(cache => cache.delete('{EscapeJsString(key)}', {{ignoreVary: true}}));",
3867 awaitPromise = true,
3868 contextId = CurrentContextId,
3869 returnByValue = true
3870 }, sessionId).GetAwaiter().GetResult()?.ToString().Contains(
"true");
3879 GPALEventType.ERROR,
3880 $
"Unsupported storage type: [{storageType}]",
3882 GPALObjectType.PuppeteerCommunicator);
3886 catch (Exception ex)
3889 GPALEventType.EXCEPTION,
3890 $
"Failed for [{storageType}] d[{domain}] sn[{storeName}] p[{path}] k[{key}]",
3892 GPALObjectType.PuppeteerCommunicator,
3896 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Deleted [{deletedCount}] [{storageType}](s)",
this, GPALObjectType.PuppeteerCommunicator);
3898 return 0 < deletedCount;
3907 private static string EscapeJsString(
string value)
3909 if (
string.IsNullOrEmpty(value))
return "";
3910 return value.Replace(
"\\",
"\\\\")
3911 .Replace(
"'",
"\\'")
3912 .Replace(
"\r",
"\\r")
3913 .Replace(
"\n",
"\\n");
3928 public async Task<dynamic> Evaluate(
string xpath,
string sessionId =
null)
3930 sessionId ??= GetEffectiveSessionId();
3931 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
3933 contextId = CurrentContextId,
3934 expression = $
"document.evaluate('{xpath}', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue"
3935 }, sessionId).ConfigureAwait(
false);
3950 public async Task<dynamic> EvaluateAll(
string xpath,
string sessionId =
null)
3952 sessionId ??= GetEffectiveSessionId();
3953 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
3955 contextId = CurrentContextId,
3956 expression = $
"Array.from(document.evaluate('{xpath}', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null))"
3957 }, sessionId).ConfigureAwait(
false);
3966 private string GetKeyCode(
char c)
3968 char upper =
char.ToUpperInvariant(c);
3970 if (upper >=
'A' && upper <=
'Z')
3972 return "Key" + upper;
3975 if (
char.IsDigit(c))
3996 return "BracketLeft";
4000 return "BracketRight";
4041 return c.ToString();
4059 public async Task<dynamic> FillIn(
string objectId,
string text,
int delayMs = 0,
string sessionId =
null)
4061 sessionId ??= GetEffectiveSessionId();
4064 bool success = await TryInsertText(objectId, text, sessionId).ConfigureAwait(
false);
4065 if (success)
return true;
4068 success = await TryJsDirectFill(objectId, text, sessionId).ConfigureAwait(
false);
4069 if (success)
return true;
4072 await FocusElement(objectId, sessionId).ConfigureAwait(
false);
4075 success = await TryTypeWithKeyEvents(objectId, text, delayMs, sessionId).ConfigureAwait(
false);
4076 if (success)
return true;
4080 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to fill element [{objectId}] with [{text}] using all methods.");
4094 private async Task<bool> TryTypeWithKeyEvents(
string objectId,
string text,
int delayMs,
string sessionId)
4098 await DispatchTextCharByChar(text, 0 < delayMs ? delayMs : 50, sessionId).ConfigureAwait(
false);
4101 string actualValue = await GetElementValue(objectId, sessionId).ConfigureAwait(
false);
4102 if (actualValue == text)
4105 await BlurElement(objectId, sessionId).ConfigureAwait(
false);
4109 catch (Exception ex)
4111 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to type text, trying next method.",
this, GPALObjectType.PuppeteerCommunicator, ex);
4124 private async Task<bool> TryInsertText(
string objectId,
string text,
string sessionId)
4129 await SendCommand(DevToolsMethods.InputInsertText,
new { text }, sessionId).ConfigureAwait(
false);
4132 string actualValue = await GetElementValue(objectId, sessionId).ConfigureAwait(
false);
4133 if (actualValue == text)
4135 await BlurElement(objectId, sessionId).ConfigureAwait(
false);
4139 catch (Exception ex)
4141 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to type text, trying next method.",
this, GPALObjectType.PuppeteerCommunicator, ex);
4155 private async Task<bool> TryJsDirectFill(
string objectId,
string text,
string sessionId)
4164 // Trigger everything frameworks listen to
4165 const desc = Object.getOwnPropertyDescriptor(this, 'value') ||
4166 Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value') ||
4167 Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value');
4169 // Bypass any framework value setters that block direct assignment
4170 if (desc && desc.set) {
4171 desc.set.call(this, value);
4174 this.dispatchEvent(new Event('input', { bubbles: true }));
4175 this.dispatchEvent(new Event('change', { bubbles: true }));
4178 await SendCommand(DevToolsMethods.RuntimeCallFunctionOn,
new
4181 functionDeclaration = js,
4182 arguments = new[] { new { value = text } },
4183 contextId = CurrentContextId,
4185 }, sessionId).ConfigureAwait(
false);
4188 string actualValue = await GetElementValue(objectId, sessionId).ConfigureAwait(
false);
4189 if (actualValue == text)
4191 await BlurElement(objectId, sessionId).ConfigureAwait(
false);
4195 catch (Exception ex)
4197 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to type text, trying next method.",
this, GPALObjectType.PuppeteerCommunicator, ex);
4209 private async Task<string> GetElementValue(
string objectId,
string sessionId)
4211 var result = await SendCommand<dynamic>(DevToolsMethods.RuntimeCallFunctionOn,
new
4214 functionDeclaration =
"function() { return this.value; }",
4215 contextId = CurrentContextId,
4216 returnByValue = true
4217 }, sessionId).ConfigureAwait(
false);
4220 return result?.result?.value?.ToString() ??
string.Empty;
4229 internal async Task FocusElement(
string objectId,
string sessionId)
4231 await SendCommand(DevToolsMethods.RuntimeCallFunctionOn,
new
4234 contextId = CurrentContextId,
4235 functionDeclaration =
"function() { this.focus(); }"
4236 }, sessionId).ConfigureAwait(
false);
4245 private async Task BlurElement(
string objectId,
string sessionId)
4247 await SendCommand(DevToolsMethods.RuntimeCallFunctionOn,
new
4250 contextId = CurrentContextId,
4251 functionDeclaration =
"function() { this.blur(); }"
4252 }, sessionId).ConfigureAwait(
false);
4267 public async Task FillInAppend(
string objectId,
string text,
int delayMs = 0,
string sessionId =
null)
4269 sessionId ??= GetEffectiveSessionId();
4271 var currentResp = await SendCommand<object>(DevToolsMethods.RuntimeCallFunctionOn,
new
4274 functionDeclaration =
"function() { return this.value || ''; }",
4275 contextId = CurrentContextId,
4276 returnByValue = true
4277 }, sessionId).ConfigureAwait(
false);
4279 string current = currentResp?.result?.value?.ToString() ??
"";
4280 await
FillIn(objectId, current + text, delayMs, sessionId).ConfigureAwait(
false);
4295 public async Task
FillInInsert(
string objectId,
string text,
int delayMs = 0,
string sessionId =
null)
4299 var currentResp = await SendCommand<object>(DevToolsMethods.RuntimeCallFunctionOn,
new
4302 functionDeclaration =
"function() { return this.value || ''; }",
4303 contextId = CurrentContextId,
4304 returnByValue = true
4305 }, sessionId).ConfigureAwait(
false);
4307 string current = currentResp?.result?.value?.ToString() ??
"";
4308 await
FillIn(objectId, text + current, delayMs, sessionId).ConfigureAwait(
false);
4323 public async Task
FillInOverwrite(
string objectId,
string text,
int delayMs = 0,
string sessionId =
null)
4325 await
FillIn(objectId, text, delayMs, sessionId).ConfigureAwait(
false);
4335 public async Task<dynamic> FireChangeEvent(
string objectId,
string sessionId =
null)
4337 sessionId ??= GetEffectiveSessionId();
4338 return await SendCommand<object>(DevToolsMethods.RuntimeCallFunctionOn,
new
4341 contextId = CurrentContextId,
4342 functionDeclaration =
"function() {{ this.dispatchEvent(new Event('change')); }}"
4343 }, sessionId).ConfigureAwait(
false);
4352 public async Task
Focus(
int backendNodeId,
string sessionId =
null)
4355 var nodeId = await GetNodeIdFromBackendNodeId(backendNodeId, sessionId).ConfigureAwait(
false);
4356 await SendCommand<object>(DevToolsMethods.DOMFocus,
new { nodeId }, sessionId).ConfigureAwait(
false);
4365 public async Task<dynamic> Forward(
string sessionId =
null)
4367 sessionId ??= GetEffectiveSessionId();
4368 dynamic history = await SendCommand<object>(DevToolsMethods.PageGetNavigationHistory,
new { }, sessionId).ConfigureAwait(
false);
4369 if (history?.currentIndex !=
null)
4371 var entries = (IEnumerable<dynamic>)history.entries;
4372 var nextEntry = entries.ElementAtOrDefault((
int)history.currentIndex + 1);
4373 if (nextEntry?.
id !=
null)
4375 var result = await SendCommand(DevToolsMethods.PageNavigateToHistoryEntry,
new { entryId = (int)nextEntry.id }, sessionId).ConfigureAwait(
false);
4379 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No next page in history",
this, GPALObjectType.PuppeteerClient);
4390 public async Task<bool> FullScreen(
string sessionId =
null)
4392 if (
true == Browser.BrowserSettings.UseHeadless)
4393 return await
Maximize().ConfigureAwait(
false);
4400 await SendCommand(DevToolsMethods.BrowserSetWindowBounds,
new
4403 bounds = new { windowState =
"fullscreen" }
4404 }, sessionId).ConfigureAwait(
false);
4418 public async Task<dynamic> GetAttribute(
int backendNodeId,
string attribute,
string sessionId =
null)
4420 var nodeId = await GetNodeIdFromBackendNodeId(backendNodeId, sessionId).ConfigureAwait(
false);
4421 sessionId ??= GetEffectiveSessionId();
4422 return await SendCommand<object>(DevToolsMethods.DOMGetAttributes,
new { nodeId, name = attribute }, sessionId).ConfigureAwait(
false);
4435 public async Task<Rectangle> GetBoundingClientRect(
int backendNodeId,
string sessionId =
null)
4437 sessionId ??= GetEffectiveSessionId();
4440 bool isInFrame = CurrentContextId.HasValue;
4441 string effectiveSessionId = isInFrame && !
string.IsNullOrEmpty(CurrentFrameSessionId)
4442 ? CurrentFrameSessionId
4450 if (CurrentContextId.HasValue)
4455 var resolveResult = await SendCommand<dynamic>(DevToolsMethods.DOMResolveNode,
new
4457 backendNodeId = backendNodeId,
4458 executionContextId = CurrentContextId.Value
4459 }, effectiveSessionId).ConfigureAwait(
false);
4461 string objectId = resolveResult?.@
object?.objectId;
4463 if (!
string.IsNullOrEmpty(objectId))
4466 var rectResult = await SendCommand<dynamic>(DevToolsMethods.RuntimeCallFunctionOn,
new
4468 objectId = objectId,
4469 functionDeclaration =
"function() { const r = this.getBoundingClientRect(); return {x: r.left, y: r.top, width: r.width, height: r.height}; }",
4470 contextId = CurrentContextId,
4471 returnByValue = true
4472 }, effectiveSessionId).ConfigureAwait(
false);
4474 if (rectResult?.result?.value !=
null)
4476 var r = rectResult.result.value;
4477 if (0 < (
int)r.width && 0 < (
int)r.height)
4479 return new Rectangle((
int)r.x, (
int)r.y, (
int)r.width, (
int)r.height);
4487 Point pageOffset = Point.Empty;
4491 pageOffset =
new Point(await WindowPageOffsetX(effectiveSessionId).ConfigureAwait(
false),
4492 await WindowPageOffsetY(effectiveSessionId).ConfigureAwait(
false));
4502 var nodeId = await GetNodeIdFromBackendNodeId(backendNodeId, effectiveSessionId).ConfigureAwait(
false);
4503 dynamic box = await SendCommand<dynamic>(DevToolsMethods.DOMGetBoxModel,
new { nodeId }, effectiveSessionId).ConfigureAwait(
false);
4507 if (boxModel?.
Model !=
null)
4514 return new Rectangle(
4522 catch (Exception ex)
4525 $
"DOM.getBoxModel failed",
4526 this, GPALObjectType.PuppeteerCommunicator, ex);
4529 return new Rectangle(-1, -1, -1, -1);
4539 public async Task<dynamic> GetContentAndCss(
string elementId,
string sessionId =
null)
4541 sessionId ??= GetEffectiveSessionId();
4542 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
4544 expression = $
"let el = document.getElementById('{elementId}'); JSON.stringify({{content: el.innerHTML, css: getComputedStyle(el)}})",
4545 contextId = CurrentContextId,
4546 returnByValue = false
4547 }, sessionId).ConfigureAwait(
false);
4556 public async Task<Dictionary<string, object>> GetCssAttributes(
int backendNodeId,
string sessionId =
null)
4558 sessionId ??= GetEffectiveSessionId();
4562 var nodeId = await GetNodeIdFromBackendNodeId(backendNodeId, sessionId).ConfigureAwait(
false);
4564 await SendCommand<object>(DevToolsMethods.DOMEnable,
new { }, sessionId).ConfigureAwait(
false);
4565 await SendCommand<object>(DevToolsMethods.CSSEnable,
new { }, sessionId).ConfigureAwait(
false);
4567 var result = await SendCommand<dynamic>(DevToolsMethods.CSSGetComputedStyleForNode,
new { nodeId }, sessionId).ConfigureAwait(
false);
4569 if (result?.computedStyle ==
null)
4571 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"CSS.getComputedStyleForNode returned null for nodeId [{nodeId}]",
this, GPALObjectType.PuppeteerCommunicator);
4575 var relevantProps =
new HashSet<string>(
new[]
4577 "display",
"visibility",
"opacity",
"pointer-events",
"overflow",
"clip-path",
4578 "position",
"top",
"left",
"right",
"bottom",
"z-index",
4579 "width",
"height",
"min-width",
"min-height",
"max-width",
"max-height",
4580 "margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
4581 "padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
4582 "border",
"border-width",
"border-style",
"border-color",
"box-sizing",
4583 "flex",
"flex-direction",
"flex-wrap",
"flex-grow",
"flex-shrink",
"flex-basis",
4584 "justify-content",
"align-items",
"align-self",
"order",
4585 "grid",
"grid-template",
"grid-area",
"grid-column",
"grid-row",
4586 "transform",
"transform-origin",
"transition",
"animation",
4587 "cursor",
"user-select",
"content",
"filter"
4590 var emptyValues =
new HashSet<string> {
"",
"none",
"normal",
"inherit",
"initial",
"unset",
"auto",
"0px",
"0",
"transparent" };
4592 var computedStyles =
new Dictionary<string, object>();
4593 foreach (var style
in result.computedStyle)
4595 string name = style.name?.ToString();
4596 string value = style.value?.ToString()?.Trim();
4598 if (name !=
null && relevantProps.Contains(name) && !
string.IsNullOrEmpty(value) && !emptyValues.Contains(value))
4600 computedStyles[name] = value;
4604 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Retrieved [{computedStyles.Count}] filtered CSS attributes for nodeId [{nodeId}]",
this, GPALObjectType.PuppeteerCommunicator);
4605 return computedStyles;
4607 catch (Exception ex)
4609 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to get CSS attributes",
this, GPALObjectType.PuppeteerCommunicator, ex);
4614 await SendCommand<object>(DevToolsMethods.CSSDisable,
new { }, sessionId).ConfigureAwait(
false);
4615 await SendCommand<object>(DevToolsMethods.DOMDisable,
new { }, sessionId).ConfigureAwait(
false);
4669 public async Task<Dictionary<string, object>>
GetDomAttributes(
int backendNodeId,
string sessionId =
null)
4676 var nodeId = await GetNodeIdFromBackendNodeId(backendNodeId, sessionId).ConfigureAwait(
false);
4677 var result = await SendCommand<object>(DevToolsMethods.DOMGetAttributes,
new
4680 }, sessionId).ConfigureAwait(
false);
4682 if (result?.attributes ==
null)
4684 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"DOM.getAttributes returned null for nodeId [{nodeId}]",
this, GPALObjectType.PuppeteerCommunicator);
4689 var attributes =
new Dictionary<string, object>();
4690 for (
int i = 0; i < result.attributes.Count - 1; i += 2)
4692 attributes[result.attributes[i]?.ToString()] = result.attributes[i + 1]?.ToString();
4695 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Retrieved [{attributes.Count}] DOM attributes for nodeId [{nodeId}]",
this, GPALObjectType.PuppeteerCommunicator);
4698 catch (Exception ex)
4700 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to get DOM attributes for nodeId [{backendNodeId}]",
this, GPALObjectType.PuppeteerCommunicator, ex);
4711 public async Task<Dictionary<string, object>> GetDomProperties(
int backendNodeId,
string sessionId =
null)
4713 sessionId ??= GetEffectiveSessionId();
4719 var resolve = await SendCommand<dynamic>(DevToolsMethods.DOMResolveNode,
new { backendNodeId, pierce = true }, sessionId).ConfigureAwait(
false);
4720 string objectId = resolve.@
object.objectId;
4723 var jsProps = await SendCommand<dynamic>(DevToolsMethods.RuntimeCallFunctionOn,
new
4726 functionDeclaration =
@"
4729 if ('textContent' in this) props.textContent = this.textContent;
4730 if ('innerHTML' in this) props.innerHTML = this.innerHTML;
4731 if ('innerText' in this) props.innerText = this.innerText;
4732 if ('outerHTML' in this) props.outerHTML = this.outerHTML;
4733 if ('className' in this) props.className = this.className;
4734 if ('disabled' in this) props.disabled = this.disabled;
4737 contextId = CurrentContextId,
4738 returnByValue = true
4739 }, sessionId).ConfigureAwait(
false);
4742 await SendCommand<dynamic>(DevToolsMethods.RuntimeReleaseObject,
4744 contextId = CurrentContextId,
4746 }, sessionId).ConfigureAwait(
false);
4748 var properties =
new Dictionary<string, object>();
4749 if (jsProps?.result?.value !=
null)
4750 properties = ((Newtonsoft.Json.Linq.JObject)jsProps.result.value).ToObject<Dictionary<string, object>>();
4753 properties = properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value ??
"");
4755 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Retrieved [{properties.Count}] DOM properties for backendNodeId [{backendNodeId}]",
this, GPALObjectType.PuppeteerCommunicator);
4758 catch (Exception ex)
4760 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to get DOM properties for backendNodeId [{backendNodeId}]",
this, GPALObjectType.PuppeteerCommunicator, ex);
5095 public async Task<string>
GetCurrentUrl(
string sessionId =
null)
5100 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
5102 expression =
"window.location.href",
5103 contextId = CurrentContextId,
5104 returnByValue = true
5105 }, sessionId).ConfigureAwait(
false);
5108 return result?.result?.value;
5117 public async Task<int> GetCurrentWindow(
string sessionId =
null)
5119 sessionId ??= GetEffectiveSessionId();
5121 var result = await SendCommand<dynamic>(
5122 DevToolsMethods.BrowserGetWindowForTarget,
5125 ).ConfigureAwait(
false);
5127 return (
int)result.windowId;
5136 public async Task<System.Drawing.Rectangle> GetWindowRectangle(
string sessionId =
null)
5138 sessionId ??= GetEffectiveSessionId();
5140 int windowId = await GetCurrentWindow(sessionId).ConfigureAwait(
false);
5142 dynamic result = await SendCommand<dynamic>(
5143 DevToolsMethods.BrowserGetWindowBounds,
5146 ).ConfigureAwait(
false);
5148 dynamic bounds = result?.bounds;
5151 return new System.Drawing.Rectangle(
5152 (
int)(bounds?.left ?? 0),
5153 (
int)(bounds?.top ?? 0),
5154 (
int)(bounds?.width ?? 0),
5155 (
int)(bounds?.height ?? 0));
5165 public async Task<dynamic> GetElementAttributeHash(
string elementId,
string sessionId =
null)
5167 sessionId ??= GetEffectiveSessionId();
5168 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
5171 expression =
"JSON.stringify(document.getElementById('" + elementId +
"').attributes)",
5172 contextId = CurrentContextId,
5173 returnByValue = false
5174 }, sessionId).ConfigureAwait(
false);
5189 public async Task<string> GetPageSource(
string sessionId =
null)
5191 if (sessionId ==
null)
5193 sessionId = GetEffectiveSessionId();
5197 var docResult = await SendCommand(
5198 DevToolsMethods.DOMGetDocument,
5199 new Dictionary<string, object> { {
"depth", 0 } },
5200 sessionId).ConfigureAwait(
false);
5202 if (docResult ==
null)
5204 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"DOM.getDocument returned null",
5205 this, GPALObjectType.PuppeteerCommunicator);
5206 return string.Empty;
5210 JObject rootDoc = docResult as JObject;
5211 if (rootDoc ==
null)
5215 string jsonStr = Newtonsoft.Json.JsonConvert.SerializeObject(docResult);
5216 rootDoc = JObject.Parse(jsonStr);
5218 catch (Exception ex)
5220 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
5221 "Failed to convert DOM.getDocument response to JObject: [" + ex.Message +
"]",
5222 this, GPALObjectType.PuppeteerCommunicator);
5223 return string.Empty;
5228 JToken rootToken = rootDoc[
"root"] ?? rootDoc[
"result"]?[
"root"];
5230 if (rootToken ==
null || rootToken.Type != JTokenType.Object)
5232 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
5233 "No valid 'root' object found in DOM.getDocument response",
5234 this, GPALObjectType.PuppeteerCommunicator);
5235 return string.Empty;
5238 JObject root = (JObject)rootToken;
5240 if (!
long.TryParse(root[
"nodeId"]?.ToString(), out
long rootNodeId))
5242 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Invalid or missing nodeId in root",
5243 this, GPALObjectType.PuppeteerCommunicator);
5244 return string.Empty;
5248 var htmlResult = await SendCommand(
5249 DevToolsMethods.DOMGetOuterHTML,
5250 new Dictionary<string, object> { {
"nodeId", rootNodeId } },
5251 sessionId).ConfigureAwait(
false);
5253 if (htmlResult ==
null)
5255 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"DOM.getOuterHTML returned null",
5256 this, GPALObjectType.PuppeteerCommunicator);
5257 return string.Empty;
5261 JObject htmlDoc = htmlResult as JObject;
5262 if (htmlDoc ==
null)
5266 string jsonStr = Newtonsoft.Json.JsonConvert.SerializeObject(htmlResult);
5267 htmlDoc = JObject.Parse(jsonStr);
5269 catch (Exception ex)
5272 "Failed to convert DOM.getOuterHTML response to JObject: [" + ex.Message +
"]",
5273 this, GPALObjectType.PuppeteerCommunicator);
5274 return string.Empty;
5279 string fullSource = htmlDoc[
"outerHTML"]?.ToString();
5281 if (
string.IsNullOrWhiteSpace(fullSource))
5284 this, GPALObjectType.PuppeteerCommunicator);
5285 return string.Empty;
5302 public async Task<dynamic> GetParentNode(
string elementId,
string sessionId =
null)
5304 sessionId ??= GetEffectiveSessionId();
5305 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
5307 contextId = CurrentContextId,
5308 expression =
"document.getElementById('" + elementId +
"').parentNode"
5309 }, sessionId).ConfigureAwait(
false);
5320 public async Task<string> GetReadyStatus(
string sessionId,
string sessionToken)
5322 sessionId ??= GetEffectiveSessionId();
5323 string retVal =
null;
5325 if (lastSessionToken != sessionToken)
5327 lastErrorMessage.Clear();
5328 supressedMessage =
false;
5329 lastSessionToken = sessionToken;
5332 JObject result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
new
5334 expression =
"document.readyState",
5335 contextId = CurrentContextId,
5336 returnByValue = false
5337 }, sessionId).ConfigureAwait(
false);
5339 if (
null != result?[
"result"]?[
"value"])
5340 retVal = result?[
"result"]?[
"value"]?.ToString();
5342 retVal =
@"""loading""";
5344 string currentErrorMessage = $
"Document.Ready status [{retVal}]";
5346 if (
false == lastErrorMessage.Contains(currentErrorMessage))
5348 lastErrorMessage.Add(currentErrorMessage);
5350 supressedMessage =
false;
5352 else if (
false == supressedMessage)
5355 supressedMessage =
true;
5368 public async Task<dynamic> GetShadowRoot(
string css,
string sessionId =
null)
5370 sessionId ??= GetEffectiveSessionId();
5371 var nodeId = await QueryByCss(css, sessionId).ConfigureAwait(
false);
5372 return await SendCommand<object>(DevToolsMethods.DOMDescribeNode,
new { nodeId, pierce = true }, sessionId).ConfigureAwait(
false);
5398 public async Task<string> GetStorage(
5401 string domain =
null,
5402 string storeName =
null,
5406 sessionId ??= GetEffectiveSessionId();
5407 string resultJson =
"null";
5411 switch (storageType)
5416 dynamic cookiesResp = await SendCommand<dynamic>(DevToolsMethods.StorageGetCookies,
5418 sessionId).ConfigureAwait(
false);
5420 if (cookiesResp?.cookies !=
null)
5422 var filtered =
new List<dynamic>();
5425 foreach (dynamic c
in cookiesResp.cookies)
5427 if ((
false == UrlHelper.CookieDomainMatches(((dynamic)c).domain?.ToString(), domain)) ||
5428 (!
string.IsNullOrEmpty(path) && ((dynamic)c).path != path) ||
5429 (!
string.IsNullOrEmpty(key) && ((dynamic)c).name != key))
5434 if (((dynamic)c).partitionKey !=
null)
5438 Key = ((dynamic)c).name,
5439 Domain = ((dynamic)c).domain,
5440 Path = ((dynamic)c).path,
5441 Value = ((dynamic)c).value,
5442 Expires = ((dynamic)c).expires,
5444 Secure = ((dynamic)c).secure,
5445 HttpOnly = ((dynamic)c).httpOnly,
5446 SameSite = ((dynamic)c).sameSite,
5447 Session = ((dynamic)c).session,
5448 Priority = ((dynamic)c).priority,
5449 SameParty = ((dynamic)c).sameParty,
5450 SourceScheme = ((dynamic)c).sourceScheme,
5451 SourcePort = ((dynamic)c).sourcePort,
5453 PartitionKey = ((dynamic)c).partitionKey
5460 Key = ((dynamic)c).name,
5461 Domain = ((dynamic)c).domain,
5462 Path = ((dynamic)c).path,
5463 Value = ((dynamic)c).value,
5464 Expires = ((dynamic)c).expires,
5466 Secure = ((dynamic)c).secure,
5467 HttpOnly = ((dynamic)c).httpOnly,
5468 SameSite = ((dynamic)c).sameSite,
5469 Session = ((dynamic)c).session,
5470 Priority = ((dynamic)c).priority,
5471 SameParty = ((dynamic)c).sameParty,
5472 SourceScheme = ((dynamic)c).sourceScheme,
5473 SourcePort = ((dynamic)c).sourcePort
5478 resultJson = JsonConvert.SerializeObject(filtered);
5482 case "localStorage":
5483 case "sessionStorage":
5485 string js =
string.IsNullOrEmpty(key)
5488 for(let i=0;i<{storageType}.length;i++){{ let k={storageType}.key(i); obj[k]={storageType}.getItem(k); }}
5489 return JSON.stringify(obj);
5491 : $
"{storageType}.getItem('{EscapeJsString(key)}');";
5493 var evalResult = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
5496 awaitPromise = true,
5497 contextId = CurrentContextId,
5498 returnByValue = true
5499 }, sessionId).ConfigureAwait(
false);
5501 resultJson = evalResult?.result?.value?.ToString() ??
"null";
5508 if (
string.IsNullOrEmpty(path))
5513 let dbs = await indexedDB.databases();
5514 return JSON.stringify(dbs.map(db => ({ name: db.name, version: db.version })));
5517 else if (
string.IsNullOrEmpty(storeName))
5522 let req = indexedDB.open('{EscapeJsString(path)}');
5523 return new Promise((resolve) => {{
5524 req.onsuccess = e => {{
5525 let db = e.target.result;
5526 let stores = Array.from(db.objectStoreNames);
5528 resolve(JSON.stringify(stores));
5530 req.onerror = () => resolve(null);
5534 else if (
string.IsNullOrEmpty(key))
5539 let req = indexedDB.open('{EscapeJsString(path)}');
5540 return new Promise((resolve) => {{
5541 req.onsuccess = e => {{
5542 let db = e.target.result;
5543 let tx = db.transaction('{EscapeJsString(storeName)}', 'readonly');
5544 let store = tx.objectStore('{EscapeJsString(storeName)}');
5546 let cursorReq = store.openKeyCursor();
5547 cursorReq.onsuccess = function(event) {{
5548 let cursor = event.target.result;
5550 keys.push(cursor.key);
5554 resolve(JSON.stringify(keys));
5557 cursorReq.onerror = () => {{ db.close(); resolve(null); }};
5559 req.onerror = () => resolve(null);
5568 let req = indexedDB.open('{EscapeJsString(path)}');
5569 return new Promise((resolve) => {{
5570 req.onsuccess = e => {{
5571 let db = e.target.result;
5572 let tx = db.transaction('{EscapeJsString(storeName)}', 'readonly');
5573 let store = tx.objectStore('{EscapeJsString(storeName)}');
5574 let getReq = store.get('{EscapeJsString(key)}');
5575 getReq.onsuccess = () => {{
5577 resolve(JSON.stringify(getReq.result));
5579 getReq.onerror = () => {{ db.close(); resolve(null); }};
5581 req.onerror = () => resolve(null);
5586 var evalDb = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
5589 awaitPromise = true,
5590 contextId = CurrentContextId,
5591 returnByValue = true
5592 }, sessionId).ConfigureAwait(
false);
5594 resultJson = evalDb?.result?.value?.ToString() ??
"null";
5602 if (
string.IsNullOrEmpty(storeName))
5606 js =
string.IsNullOrEmpty(key)
5607 ?
@"(async ()=>{let c=await caches.keys(); return JSON.stringify(c);})()"
5608 :
$@"(async () => {{
5609 const response = await caches.match('{EscapeJsString(key)}');
5611 const data = await response.json();
5612 return JSON.stringify(data);
5617 else if (
string.IsNullOrEmpty(key))
5620 js =
$@"(async () => {{
5621 const cache = await caches.open('{EscapeJsString(storeName)}');
5622 const keys = await cache.keys();
5623 const keyUrls = keys.map(k => k.url); // keys are Request objects; we return clean URLs
5624 return JSON.stringify(keyUrls);
5630 js =
$@"(async () => {{
5631 const cache = await caches.open('{EscapeJsString(storeName)}');
5632 const response = await cache.match('{EscapeJsString(key)}');
5634 const contentType = response.headers.get('content-type') || '';
5636 if (contentType.includes('application/json') || contentType.includes('text/json')) {{{{
5637 data = await response.json();
5639 data = await response.text();
5641 return JSON.stringify(data);
5647 var evalCache = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
5650 awaitPromise = true,
5651 contextId = CurrentContextId,
5652 returnByValue = true
5653 }, sessionId).ConfigureAwait(
false);
5655 resultJson = evalCache?.result?.value?.ToString() ??
"null";
5660 GPAL.PublishSimpleEvent(
5661 GPALEventType.WARNING,
5662 $
"Nothing to do because StorageType is [{storageType}] d[{domain}] sn[{storeName}] p[{path}] k[{key}]",
5664 GPALObjectType.PuppeteerCommunicator);
5668 GPAL.PublishSimpleEvent(
5669 GPALEventType.ERROR,
5670 $
"Unsupported storage type: [{storageType}]",
5672 GPALObjectType.PuppeteerCommunicator);
5676 catch (Exception ex)
5678 GPAL.PublishSimpleEvent(
5679 GPALEventType.EXCEPTION,
5680 $
"GetStorage failed for [{storageType}] d[{domain}] sn[{storeName}] p[{path}] k[{key}]",
5682 GPALObjectType.PuppeteerCommunicator,
5694 public async Task<string> GetUserAgent(
string sessionId =
null)
5696 sessionId ??= GetEffectiveSessionId();
5697 JObject result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
5700 expression =
"navigator.userAgent",
5701 contextId = CurrentContextId,
5702 returnByValue = true
5703 }, sessionId).ConfigureAwait(
false);
5704 return result?[
"result"]?[
"value"]?.ToString();
5719 public async Task<bool>
GoTo(
string url,
string sessionId =
null)
5722 return await NavigateToUrl(url, sessionId).ConfigureAwait(
false);
5732 public async Task<string>
GoToTab(
object tabIdOrUrlOrIndex,
string sessionId =
null)
5734 string targetId =
null;
5735 var windowArray = _windowSessionsQueue.ToArray();
5737 if (tabIdOrUrlOrIndex is
int index)
5739 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
5741 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No active window for GoToTab",
this, GPALObjectType.PuppeteerCommunicator);
5745 var tabQueue = windowArray[_activeWindowIndex].TabQueue.ToArray();
5746 if (index < 0 || index >= tabQueue.Length)
5748 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Invalid tab index [{index}]",
this, GPALObjectType.PuppeteerCommunicator);
5753 targetId = tabQueue[index].Key;
5754 if (
string.IsNullOrEmpty(targetId))
5756 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No targetId at index [{index}]",
this, GPALObjectType.PuppeteerCommunicator);
5763 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId },
null).ConfigureAwait(
false);
5764 SetActiveTabIndex(index);
5769 else if (tabIdOrUrlOrIndex is
string str)
5771 targetId = await GetTargetTabIdByUrl(str, sessionId).ConfigureAwait(
false);
5772 if (
string.IsNullOrEmpty(targetId))
5775 foreach (var window
in windowArray)
5777 var tabArray = window.TabQueue.ToArray();
5778 if (tabArray.Any(kvp => kvp.Key == str))
5786 if (
string.IsNullOrEmpty(targetId))
5788 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No target found for tabId or URL [{str}]",
this, GPALObjectType.PuppeteerCommunicator);
5792 int windowIndex = GetCurrentWindowIndex(targetId);
5793 if (windowIndex < 0)
5795 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Tab with targetId [{targetId}] not found in any window",
this, GPALObjectType.PuppeteerCommunicator);
5799 SetActiveWindowIndex(windowIndex);
5800 var tabQueue = windowArray[windowIndex].TabQueue.ToArray();
5801 int tabIndex = Array.FindIndex(tabQueue, kvp => kvp.Key == targetId);
5804 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Tab with targetId [{targetId}] not found in window [{windowArray[windowIndex].BrowserContextId}]",
this, GPALObjectType.PuppeteerCommunicator);
5808 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId },
null).ConfigureAwait(
false);
5809 SetActiveTabIndex(tabIndex);
5811 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Switched to tab with targetId [{targetId}], windowIndex [{windowIndex}], tabIndex [{tabIndex}]",
this, GPALObjectType.PuppeteerCommunicator);
5815 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Invalid input for GoToTab [{tabIdOrUrlOrIndex}]",
this, GPALObjectType.PuppeteerCommunicator);
5826 public async Task<dynamic> HideElement(
int backendNodeId,
string sessionId =
null)
5828 return await SetAttribute(backendNodeId,
"style",
"display: none", sessionId).ConfigureAwait(
false);
5838 public async Task Hover(
int backendNodeId,
string sessionId =
null)
5840 sessionId ??= GetEffectiveSessionId();
5841 var x = await GetElementRandomCenterX(backendNodeId, sessionId).ConfigureAwait(
false);
5842 var y = await GetElementRandomCenterY(backendNodeId, sessionId).ConfigureAwait(
false);
5843 await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new { type =
"mouseMoved", button =
"none", x, y }, sessionId).ConfigureAwait(
false);
5858 public async Task<bool>
IsClickable(
string elementId,
string sessionId =
null)
5862 List<GPALElement> elems = await
EvaluateSelector(elementId, sessionId).ConfigureAwait(
false);
5863 List<dynamic> responses =
new List<dynamic>();
5865 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
5867 contextId = CurrentContextId,
5868 expression =
"document.getElementById('" + elementId +
"').checkVisibility()"
5869 }, sessionId).ConfigureAwait(
false);
5870 return (
bool)result.value;
5885 public async Task<bool> IsDisplayed(
string elementId,
string sessionId =
null)
5887 sessionId ??= GetEffectiveSessionId();
5888 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
5890 contextId = CurrentContextId,
5891 expression =
"document.getElementById('" + elementId +
"').offsetParent !== null"
5892 }, sessionId).ConfigureAwait(
false);
5893 return (
bool)result.value;
5908 public async Task<bool> IsEnabled(
string elementId,
string sessionId =
null)
5910 sessionId ??= GetEffectiveSessionId();
5911 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
5913 contextId = CurrentContextId,
5914 expression =
"!document.getElementById('" + elementId +
"').disabled"
5915 }, sessionId).ConfigureAwait(
false);
5916 return (
bool)result.value;
5926 public async Task<bool> IsEndOfPage(
string sessionId =
null)
5928 sessionId ??= GetEffectiveSessionId();
5932 var result = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
5937 const overflow = getComputedStyle(document.documentElement).overflow;
5938 if (overflow !== 'hidden') {
5939 return (window.innerHeight + window.scrollY) >= document.body.scrollHeight;
5941 const scrollable = Array.from(document.querySelectorAll('*')).find(
5942 el => el.scrollHeight > el.clientHeight && getComputedStyle(el).overflowY !== 'hidden'
5944 return (scrollable.clientHeight + scrollable.scrollTop) > scrollable.scrollHeight;
5949 contextId = CurrentContextId,
5950 returnByValue = true
5951 }, sessionId).ConfigureAwait(
false);
5954 return (
bool)result?[
"result"]?[
"value"];
5956 catch (Exception ex)
5958 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Error detecting end of page, returning [true]",
this, GPALObjectType.PuppeteerCommunicator, ex);
5981 public async Task<bool> ElementFromPoint(
GPALElement element,
int x,
int y,
string sessionId =
null)
5983 sessionId ??= GetEffectiveSessionId();
5988 var resolved = await SendCommand<dynamic>(DevToolsMethods.DOMResolveNode,
new
5990 backendNodeId = element.ElementBackendNodeId
5991 }, sessionId).ConfigureAwait(
false);
5993 string objectId = resolved?.@
object?.objectId;
5995 if (
true ==
string.IsNullOrEmpty(objectId))
6002 var result = await SendCommand<dynamic>(DevToolsMethods.RuntimeCallFunctionOn,
new
6004 objectId = objectId,
6005 functionDeclaration =
@"function (x, y) {
6006 const hit = document.elementFromPoint(x, y);
6007 if (hit === this || this.contains(hit)) { return ''; }
6008 if (!hit) { return 'nothing'; }
6010 + (hit.id ? '#' + hit.id : '')
6011 + (hit.className ? '.' + String(hit.className).trim().split(/\s+/).join('.') : '');
6013 arguments = new object[] { new { value = x }, new { value = y } },
6014 returnByValue =
true
6015 }, sessionId).ConfigureAwait(
false);
6017 string covering = result?.result?.value as string;
6019 if (
false ==
string.IsNullOrEmpty(covering))
6020 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"[{covering}] is what is at viewport [{x}, {y}], not [{element.TagName}]",
null, GPALObjectType.Browser);
6022 return true ==
string.IsNullOrEmpty(covering) &&
null != result?.result?.value;
6025 public async Task<bool> IsVisibleInViewport(dynamic elemsOrBackendNodeId,
string sessionId =
null)
6027 sessionId ??= GetEffectiveSessionId();
6028 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
new
6031 height: window.innerHeight || document.documentElement.clientHeight,
6032 width: window.innerWidth || document.documentElement.clientWidth
6034 contextId = CurrentContextId,
6035 returnByValue = true
6036 }, sessionId).ConfigureAwait(
false);
6038 int viewportHeight = 0;
6039 int viewportWidth = 0;
6040 bool isInViewport =
true;
6041 if (result?.result?.value !=
null)
6043 var dict = ((Newtonsoft.Json.Linq.JObject)result.result.value).ToObject<Dictionary<string, int>>();
6044 viewportHeight = dict[
"height"];
6045 viewportWidth = dict[
"width"];
6048 if (elemsOrBackendNodeId is List<GPALElement>)
6049 foreach (GPALElement element
in elemsOrBackendNodeId)
6051 Rectangle rect = await GetBoundingClientRect(element.ElementBackendNodeId, sessionId).ConfigureAwait(
false);
6053 isInViewport =
true;
6055 isInViewport &= rect.Top >= 0 &&
6057 rect.Bottom <= viewportHeight &&
6058 rect.Right <= viewportWidth;
6062 Rectangle rect = await GetBoundingClientRect(elemsOrBackendNodeId, sessionId).ConfigureAwait(
false);
6064 isInViewport =
true;
6066 isInViewport &= rect.Top >= 0 &&
6068 rect.Bottom <= viewportHeight &&
6069 rect.Right <= viewportWidth;
6071 return isInViewport;
6084 public async Task ClickElement(GPALElement element,
string sessionId, List<dynamic> responses,
6085 ClickType clickType = ClickType.LeftClick,
6088 bool cdpSuccess =
false;
6089 bool elementNotRendered =
false;
6090 bool forceJavscript =
false;
6091 bool jsSuccess =
false;
6092 bool isInFrame = CurrentContextId.HasValue;
6093 string effectiveSessionId = isInFrame && !
string.IsNullOrEmpty(CurrentFrameSessionId)
6094 ? CurrentFrameSessionId
6102 if (
true == Browser.BrowserSettings.UseHeadless)
6104 await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
new
6106 expression =
"document.body.offsetHeight;",
6107 contextId = CurrentContextId,
6108 returnByValue = true
6109 }, sessionId).ConfigureAwait(
false);
6111 await Task.Delay(80).ConfigureAwait(
false);
6114 var boxModel = await SendCommand<object>(DevToolsMethods.DOMGetBoxModel,
new
6116 objectId = element.ElementHandle
6117 }, effectiveSessionId).ConfigureAwait(
false);
6119 if (
null != boxModel)
6121 var contentQuad = boxModel?.model?.content;
6122 if (contentQuad ==
null || contentQuad.Count < 8)
6124 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"[{element.TagName}][{clickType}] failed: Unable to get coordinates (boxmodel)");
6128 float x = (float)contentQuad[0];
6129 float y = (float)contentQuad[1];
6130 float width = (float)(contentQuad[2] - contentQuad[0]);
6131 float height = (float)(contentQuad[7] - contentQuad[1]);
6133 var random =
new Random();
6135 var clickAreaWidth = width * (1f - 2 * margin);
6136 var clickAreaHeight = height * (1f - 2 * margin);
6138 x = x + width * margin + (float)random.NextDouble() * clickAreaWidth;
6139 y = y + height * margin + (float)random.NextDouble() * clickAreaHeight;
6144 if (
"A".Equals(element.TagName, StringComparison.OrdinalIgnoreCase))
6146 x = (float)contentQuad[0] + width / 2f;
6147 y = (float)contentQuad[1] + height / 2f;
6151 string button = clickType
switch
6153 ClickType.LeftClick =>
"left",
6154 ClickType.MiddleClick =>
"middle",
6155 ClickType.RightClick =>
"right",
6156 ClickType.LeftDoubleClick =>
"left",
6160 int clickCount = clickType == ClickType.LeftDoubleClick ? 2 : 1;
6163 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6165 type =
"mousePressed",
6167 buttons = PressedButtons(button),
6172 }, effectiveSessionId).ConfigureAwait(
false));
6174 await Task.Delay(random.Next(60, 140)).ConfigureAwait(
false);
6177 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6179 type =
"mouseReleased",
6182 x = x + random.Next(-2, 3),
6183 y = y + random.Next(-2, 3),
6185 }, effectiveSessionId).ConfigureAwait(
false));
6192 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);
6193 forceJavscript =
true;
6200 elementNotRendered = 0 >= element.BoundingRect.Width || 0 >= element.BoundingRect.Height;
6202 float x = (float)element.BoundingRect.X;
6203 float y = (float)element.BoundingRect.Y;
6204 float width = (float)element.BoundingRect.Width;
6205 float height = (float)element.BoundingRect.Height;
6207 var random =
new Random();
6209 var clickAreaWidth = width * (1f - 2 * margin);
6210 var clickAreaHeight = height * (1f - 2 * margin);
6212 x = x + width * margin + (float)random.NextDouble() * clickAreaWidth;
6213 y = y + height * margin + (float)random.NextDouble() * clickAreaHeight;
6218 if (
"A".Equals(element.TagName, StringComparison.OrdinalIgnoreCase))
6220 x = (float)element.BoundingRect.X + width / 2f;
6221 y = (float)element.BoundingRect.Y + height / 2f;
6225 string button = clickType
switch
6227 ClickType.LeftClick =>
"left",
6228 ClickType.MiddleClick =>
"middle",
6229 ClickType.RightClick =>
"right",
6230 ClickType.LeftDoubleClick =>
"left",
6234 int clickCount = clickType == ClickType.LeftDoubleClick ? 2 : 1;
6237 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6239 type =
"mousePressed",
6241 buttons = PressedButtons(button),
6246 }, effectiveSessionId).ConfigureAwait(
false));
6248 await Task.Delay(random.Next(60, 140)).ConfigureAwait(
false);
6251 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6253 type =
"mouseReleased",
6256 x = x + random.Next(-2, 3),
6257 y = y + random.Next(-2, 3),
6259 }, effectiveSessionId).ConfigureAwait(
false));
6262 else if (
true == elementNotRendered)
6264 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"CDP [{clickType}] has no point to click for [{element.TagName}][{element.Css}]. Element is not rendered, so it has no position on the page. Falling back to JavaScript.",
this, GPALObjectType.PuppeteerClient);
6265 forceJavscript =
true;
6269 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"CDP [{clickType}] failed. Falling back to JavaScript.",
this, GPALObjectType.PuppeteerClient);
6270 forceJavscript =
true;
6274 catch (Exception ex)
6276 if (!GPAL.NoFallbackRecoveryActions)
6277 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"CDP [{clickType}] failed.{(false == GPAL.NoFallbackRecoveryActions ? " Falling back to JavaScript.
" : " Javascript fallback disallowed.
")}",
this, GPALObjectType.PuppeteerClient, ex);
6281 if (
false == GPAL.NoFallbackRecoveryActions && (
false == cdpSuccess ||
true == forceJavscript))
6283 bool byCss =
false ==
string.IsNullOrEmpty(element.Css);
6284 string selector =
true == byCss ? element.Css : element.Xpath;
6286 if (
string.IsNullOrEmpty(selector))
6287 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Fallback JavaScript failed: No valid XPath or CSS selector for [{element.TagName}]",
this, GPALObjectType.PuppeteerClient);
6289 var clickScript =
$@"
6292 const sel = `{selector.Replace("'", "\\'")}`;
6294 // looked up as the kind of selector that was actually passed. handing a css selector to
6295 // document.evaluate throws, and the throw skipped the lookup that would have worked
6296 const el = {(true == byCss ? "document.querySelector(sel)
" : "document.evaluate(sel, document,
null, XPathResult.FIRST_ORDERED_NODE_TYPE,
null).singleNodeValue
")};
6297 if (!el) return false;
6299 // getBoundingClientRect is already viewport relative, which is what clientX and
6300 // clientY are defined as. adding the scroll offset turns them into document
6301 // coordinates and any handler that hit tests the event reads the wrong point
6302 const rect = el.getBoundingClientRect();
6303 const cx = rect.left + rect.width / 2;
6304 const cy = rect.top + rect.height / 2;
6306 const opts = {{ bubbles: true, cancelable: true, clientX: cx, clientY: cy, view: window }};
6308 // === LEFT CLICK (or double) ===
6309 if ({(clickType == ClickType.LeftClick || clickType == ClickType.LeftDoubleClick).ToString().ToLower()}) {{
6310 const count = {(clickType == ClickType.LeftDoubleClick ? 2 : 1)};
6311 for (let i = 0; i < count; i++) {{
6312 el.dispatchEvent(new MouseEvent('mousedown', {{...opts, button: 0, buttons: 1}}));
6313 el.dispatchEvent(new MouseEvent('mouseup', {{...opts, button: 0, buttons: 0}}));
6314 el.dispatchEvent(new MouseEvent('click', {{...opts, button: 0, detail: i+1}}));
6315 if (i === 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 80); // ~80ms between clicks
6320 // === MIDDLE CLICK (eBay pop-under) ===
6321 if ({(clickType == ClickType.MiddleClick).ToString().ToLower()}) {{
6322 el.dispatchEvent(new MouseEvent('mousedown', {{...opts, button: 1, buttons: 4}}));
6323 el.dispatchEvent(new MouseEvent('mouseup', {{...opts, button: 1, buttons: 0}}));
6324 el.dispatchEvent(new MouseEvent('click', {{...opts, button: 1}}));
6328 // === RIGHT CLICK (context menu) ===
6329 if ({(clickType == ClickType.RightClick).ToString().ToLower()}) {{
6330 el.dispatchEvent(new MouseEvent('mousedown', {{...opts, button: 2, buttons: 2}}));
6331 el.dispatchEvent(new MouseEvent('mouseup', {{...opts, button: 2, buttons: 0}}));
6332 el.dispatchEvent(new MouseEvent('contextmenu', {{...opts, button: 2}}));
6343 if (
false ==
string.IsNullOrEmpty(selector))
6345 dynamic jsResult = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
new
6347 expression = clickScript,
6348 objectGroup =
"node",
6349 contextId = CurrentContextId,
6350 returnByValue = true
6351 }, sessionId).ConfigureAwait(
false);
6353 responses.Add(jsResult);
6356 if (jsResult?.result?.value is
bool clicked && clicked)
6363 string clickPath =
true == cdpSuccess ?
"CDP" :
true == jsSuccess ?
"JavaScript" :
"nothing";
6365 GPAL.
PublishSimpleEvent(
true == cdpSuccess ||
true == jsSuccess ? GPALEventType.INFO : GPALEventType.WARNING,
6366 $
"Puppeteer [{clickType}] [{element.TagName}][{Browser.CurrentUOW.CurrentSelector.Name}] w/modifiers [{((PuppeteerClient)PuppeteerClient).GetModifiersString(modifiers)}] dispatched by [{clickPath}]",
6367 this, GPALObjectType.PuppeteerCommunicator);
6379 public async Task
DragAndDrop(
GPALElement element,
string sessionId, List<dynamic> responses,
int deltaX,
int deltaY,
int offsetX = 0,
int offsetY = 0)
6381 var startX = (float)element.
BoundingRect.
X + (
float)element.BoundingRect.Width / 2 + offsetX;
6382 var startY = (float)element.
BoundingRect.
Y + (
float)element.BoundingRect.Height / 2 + offsetY;
6383 var endX = startX + deltaX;
6384 var endY = startY + deltaY;
6386 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6388 type =
"mousePressed",
6394 }, sessionId).ConfigureAwait(
false));
6396 const int STEPS = 10;
6397 for (
int i = 1; i <= STEPS; i++)
6399 var x = startX + (endX - startX) * i / STEPS;
6400 var y = startY + (endY - startY) * i / STEPS;
6402 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6404 type =
"mouseMoved",
6410 }, sessionId).ConfigureAwait(
false));
6412 await Task.Delay(15).ConfigureAwait(
false);
6415 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6417 type =
"mouseReleased",
6423 }, sessionId).ConfigureAwait(
false));
6438 public async Task LeftClick(
string elementId,
string sessionId =
null)
6440 sessionId ??= GetEffectiveSessionId();
6441 List<GPALElement> elems = await EvaluateSelector(elementId, sessionId).ConfigureAwait(
false);
6442 List<dynamic> responses =
new List<dynamic>();
6446 await ScrollIntoView(elem, sessionId).ConfigureAwait(
false);
6447 await ClickElement(elem, sessionId, responses, ClickType.LeftClick).ConfigureAwait(
false);
6462 public async Task SetDownloadBehavior(
string downloadPath,
string sessionId =
null)
6464 sessionId ??= GetEffectiveSessionId();
6466 await SendCommand<object>(DevToolsMethods.BrowserSetDownloadBehavior,
new { behavior =
"allow", downloadPath = downloadPath }, sessionId).ConfigureAwait(
false);
6481 await SendCommand<object>(DevToolsMethods.BrowserSetDownloadBehavior,
new { behavior =
"default" }, sessionId).ConfigureAwait(
false);
6498 public async Task
LeftClickAndDownload(List<GPALElement> elems,
string downloadPath,
int modifiers,
string sessionId, List<dynamic> responses)
6502 string saveDirectory = Path.GetDirectoryName(downloadPath);
6510 await
ClickElement(elem, sessionId, responses, ClickType.LeftClick, modifiers).ConfigureAwait(
false);
6516 private static readonly Dictionary<string, List<string>> _attachedFilesPerSession =
new Dictionary<string, List<string>>();
6557 List<GPALElement> elems,
6561 List<dynamic> responses)
6566 var newPaths =
new List<string>();
6568 if (uploadFiles is
string singlePath)
6570 if (
"" != singlePath?.Trim())
6572 newPaths.Add(Path.GetFullPath(singlePath));
6575 else if (uploadFiles is
GPALFile pathEnumerable)
6577 foreach (var p
in pathEnumerable.Filenames)
6579 if (
"" != p?.Trim())
6581 newPaths.Add(Path.GetFullPath(p));
6591 if (0 == newPaths.Count)
6598 string nodeKey = $
"{sessionId}:{fileInput.ElementBackendNodeId}";
6601 string fileCountStr = fileInput.
GetAttribute(
"length");
6602 int browserFileCount = 0;
6604 if (
"" != fileCountStr)
6606 Int32.TryParse(fileCountStr, out browserFileCount);
6610 if (
false == _attachedFilesPerSession.TryGetValue(nodeKey, out var currentFiles))
6612 currentFiles =
new List<string>();
6613 _attachedFilesPerSession[nodeKey] = currentFiles;
6617 if (browserFileCount != currentFiles.Count)
6619 currentFiles.Clear();
6620 _attachedFilesPerSession[nodeKey] = currentFiles;
6624 currentFiles.AddRange(newPaths);
6626 var setFilesParams =
new
6628 nodeId = fileInput.ElementNodeId,
6629 files = currentFiles.ToArray()
6632 await SendCommand<object>(DevToolsMethods.DOMSetFileInputFiles, setFilesParams, sessionId).ConfigureAwait(
false);
6633 await FireChangeEvent(fileInput.ElementHandle, sessionId).ConfigureAwait(
false);
6637 status =
"upload_attached",
6638 pathsAdded = newPaths,
6639 totalFilesTracked = currentFiles.Count,
6640 browserReportedCount = browserFileCount
6654 public async Task LeftDoubleClick(
string elementId,
string sessionId =
null)
6656 sessionId ??= GetEffectiveSessionId();
6657 List<GPALElement> elems = await EvaluateSelector(elementId, sessionId).ConfigureAwait(
false);
6658 List<dynamic> responses =
new List<dynamic>();
6660 foreach (GPALElement elem
in elems)
6662 await ScrollIntoView(elem, sessionId).ConfigureAwait(
false);
6663 await ClickElement(elem, sessionId, responses, ClickType.LeftDoubleClick).ConfigureAwait(
false);
6675 public async Task<bool> Maximize(
string sessionId =
null)
6677 sessionId ??= GetEffectiveSessionId();
6679 var windowId = await GetCurrentWindow(sessionId).ConfigureAwait(
false);
6682 await SendCommand(DevToolsMethods.BrowserSetWindowBounds,
new
6685 bounds = new { windowState =
"maximized" }
6686 }, sessionId).ConfigureAwait(
false);
6698 public async Task<bool> Minimize(
string sessionId =
null)
6700 sessionId ??= GetEffectiveSessionId();
6702 var windowId = await GetCurrentWindow(sessionId).ConfigureAwait(
false);
6703 await SendCommand(DevToolsMethods.BrowserSetWindowBounds,
new
6705 windowId = windowId,
6706 bounds = new { windowState =
"minimized" }
6707 }, sessionId).ConfigureAwait(
false);
6720 public async Task<bool> MoveTo(List<GPALElement> elems,
string sessionId, List<dynamic> responses)
6728 await ScrollIntoView(element, sessionId).ConfigureAwait(
false);
6731 var random =
new Random();
6735 var x = (float)element.
BoundingRect.
X + (
float)element.BoundingRect.Width * margin + (float)random.NextDouble() * clickAreaWidth;
6736 var y = (float)element.
BoundingRect.
Y + (
float)element.BoundingRect.Height * margin + (float)random.NextDouble() * clickAreaHeight;
6738 if (
true ==
"A".Equals(element.
TagName))
6740 x = (float)element.
BoundingRect.
X + (
float)element.BoundingRect.Width / 2;
6741 y = (float)element.
BoundingRect.
Y + (
float)element.BoundingRect.Height / 2;
6744 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6746 type =
"mouseMoved",
6750 }, sessionId).ConfigureAwait(
false));
6752 catch (Exception ex)
6754 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Error moving to element [{element.ElementHandle}][{element.Css}]",
this, GPALObjectType.Puppeteer, ex);
6768 public async Task MoveTo(
int backendNodeId,
string sessionId =
null)
6770 sessionId ??= GetEffectiveSessionId();
6771 var x = await GetElementRandomCenterX(backendNodeId, sessionId).ConfigureAwait(
false);
6772 var y = await GetElementRandomCenterY(backendNodeId, sessionId).ConfigureAwait(
false);
6773 await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new { type =
"mouseMoved", x, y }, sessionId).ConfigureAwait(
false);
6795 private static int PressedButtons(
string button)
6797 int retVal =
"middle" == button ? 4 :
"right" == button ? 2 : 1;
6802 public async Task ClickElement(
int x,
int y,
string sessionId, List<dynamic> responses,
6803 ClickType clickType = ClickType.LeftClick,
6806 sessionId ??= GetEffectiveSessionId();
6808 string button = clickType
switch
6810 ClickType.LeftClick =>
"left",
6811 ClickType.MiddleClick =>
"middle",
6812 ClickType.RightClick =>
"right",
6813 ClickType.LeftDoubleClick =>
"left",
6817 int clickCount = ClickType.LeftDoubleClick == clickType ? 2 : 1;
6818 var random =
new Random();
6821 await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6823 type =
"mouseMoved",
6827 }, sessionId).ConfigureAwait(
false);
6829 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6831 type =
"mousePressed",
6833 buttons = PressedButtons(button),
6838 }, sessionId).ConfigureAwait(
false));
6840 await Task.Delay(random.Next(60, 140)).ConfigureAwait(
false);
6842 responses.Add(await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new
6844 type =
"mouseReleased",
6850 }, sessionId).ConfigureAwait(
false));
6863 public async Task MoveTo(
int x,
int y,
string sessionId =
null)
6865 sessionId ??= GetEffectiveSessionId();
6866 await SendCommand<object>(DevToolsMethods.InputDispatchMouseEvent,
new { type =
"mouseMoved", button =
"none", x, y }, sessionId).ConfigureAwait(
false);
6884 public async Task<string> NewTab(GPALUrl url =
null,
string sessionId =
null)
6887 url =
new GPALUrl();
6889 sessionId ??= GetEffectiveSessionId();
6890 url.ForUrl(MagicHelper.GetFullUrl(url.Url, Browser, out Browser._areRobotsAllowed));
6892 if (
false == Browser.AreRobotsAllowed &&
true == Browser.BrowserSettings.ObeyRobotsTxt)
6894 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);
6895 url.ForUrl(
"https://google.com");
6898 var browserContextId = GetActiveWindowId();
6899 if (
string.IsNullOrEmpty(browserContextId))
6901 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
6902 "NewTab: No active window found to attach tab",
6903 this, GPALObjectType.PuppeteerCommunicator);
6910 var result = await SendCommand<object>(DevToolsMethods.TargetCreateTarget,
6911 new { url = url.Url, browserContextId },
null).ConfigureAwait(
false);
6913 string targetId = result.targetId.ToString();
6917 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId },
null).ConfigureAwait(
false);
6919 var attached = await SendCommand<object>(DevToolsMethods.TargetAttachToTarget,
6920 new { targetId, flatten = true },
null).ConfigureAwait(
false);
6922 string tabSessionId = attached?.sessionId?.ToString();
6924 if (
true ==
string.IsNullOrEmpty(tabSessionId))
6925 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Opened tab [{targetId}] but could not attach to it, so commands still go to the tab before it",
this, GPALObjectType.PuppeteerCommunicator);
6928 await HandleNewTarget(targetId, browserContextId, tabSessionId,
false,
true).ConfigureAwait(
false);
6931 $
"Opened new tab TargetId [{targetId}] in Window [{browserContextId}]",
6932 this, GPALObjectType.PuppeteerCommunicator);
6946 internal int GetCurrentTabIndex(
string currentTargetId)
6948 var windowArray = _windowSessionsQueue.ToArray();
6949 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
6951 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No active window for NextTab",
this, GPALObjectType.PuppeteerCommunicator);
6955 var tabQueue = windowArray[_activeWindowIndex].TabQueue;
6956 var queueArray = tabQueue.ToArray();
6957 if (queueArray.Length == 0)
6959 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No tabs for NextTab",
this, GPALObjectType.PuppeteerCommunicator);
6963 var currentIndex = Array.FindIndex(queueArray, kvp => kvp.Key == currentTargetId);
6964 if (currentIndex == -1)
6965 currentIndex = windowArray[_activeWindowIndex].ActiveTabIndex;
6967 return currentIndex;
6977 public async Task<string> NextTab(
string currentTargetId)
6979 var windowArray = _windowSessionsQueue.ToArray();
6980 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
6982 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No active window for NextTab",
this, GPALObjectType.PuppeteerCommunicator);
6986 var session = windowArray[_activeWindowIndex];
6987 var tabQueue = session.TabQueue;
6988 var browserContextId = session.BrowserContextId;
6991 var targetsResponse = await SendCommand<dynamic>(
6992 DevToolsMethods.TargetGetTargets,
6995 ).ConfigureAwait(
false);
6997 if (targetsResponse?.targetInfos ==
null)
6999 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"Failed to get targets list in NextTab sync",
this, GPALObjectType.PuppeteerCommunicator);
7004 var knownTargetIds =
new HashSet<string>(tabQueue.Select(kv => kv.Key));
7006 foreach (var targetInfo
in targetsResponse.targetInfos)
7008 string targetId = targetInfo.targetId?.ToString();
7009 string type = targetInfo.type?.ToString();
7010 string contextId = targetInfo.browserContextId?.ToString();
7012 if (
string.IsNullOrEmpty(targetId) || type !=
"page" || contextId != browserContextId)
7015 if (!knownTargetIds.Contains(targetId))
7018 await AddTabToQueue(targetId,
null,
false,
true).ConfigureAwait(
false);
7019 knownTargetIds.Add(targetId);
7024 var queueArray = tabQueue.ToArray();
7025 if (queueArray.Length == 0)
7027 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No tabs for NextTab",
this, GPALObjectType.PuppeteerCommunicator);
7034 var currentIndex = Array.FindIndex(queueArray, kvp => kvp.Key == currentTargetId);
7035 if (currentIndex == -1)
7037 currentIndex = session.ActiveTabIndex;
7040 var nextIndex = (currentIndex + 1) % queueArray.Length;
7041 var nextTargetId = await GoToTab(nextIndex).ConfigureAwait(
false);
7043 session.ActiveTabIndex = nextIndex;
7045 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
7046 $
"Switched to index [{nextIndex}], ID [{nextTargetId}], Active window tab queue size [{queueArray.Length}]",
7047 this, GPALObjectType.PuppeteerCommunicator);
7049 return nextTargetId;
7087 public async Task<string>
NextWindow(
string currentTargetId)
7089 var windowArray = _windowSessionsQueue.ToArray();
7090 if (windowArray.Length == 0)
7092 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No windows for NextWindow",
this, GPALObjectType.PuppeteerCommunicator);
7096 var currentWindowIndex = -1;
7097 for (
int i = 0; i < windowArray.Length; i++)
7099 var tabArray = windowArray[i].TabQueue.ToArray();
7100 if (Array.Exists(tabArray, kvp => kvp.Key == currentTargetId))
7102 currentWindowIndex = i;
7106 if (currentWindowIndex == -1)
7108 currentWindowIndex = _activeWindowIndex;
7111 var nextWindowIndex = (currentWindowIndex + 1) % windowArray.Length;
7112 SetActiveWindowIndex(nextWindowIndex);
7114 var nextWindowContextId = windowArray[nextWindowIndex].BrowserContextId;
7115 var nextTabQueue = windowArray[nextWindowIndex].TabQueue;
7116 var nextTabArray = nextTabQueue.ToArray();
7117 if (nextTabArray.Length == 0)
7119 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"No tabs in next window for NextWindow",
this, GPALObjectType.PuppeteerCommunicator);
7123 var nextTargetId = nextTabArray[windowArray[nextWindowIndex].ActiveTabIndex].Key;
7124 var nextSessionId = nextTabArray[windowArray[nextWindowIndex].ActiveTabIndex].Value;
7126 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId = nextTargetId },
null).ConfigureAwait(
false);
7128 if (await SendCommand<object>(DevToolsMethods.PageEnable,
new { }, nextSessionId).ConfigureAwait(
false) ==
null)
7130 var newSession = await SendCommand<object>(DevToolsMethods.TargetAttachToTarget,
new { targetId = nextTargetId, flatten = true },
null).ConfigureAwait(
false);
7131 if (newSession?.sessionId !=
null)
7133 nextSessionId = (string)newSession.sessionId;
7134 RemoveTabFromQueue(nextTargetId);
7135 await AddTabToQueue(nextTargetId, nextSessionId).ConfigureAwait(
false);
7139 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to re-attach NextWindow tab ID [{nextTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
7142 await SendCommand<object>(DevToolsMethods.PageDisable,
new { }, nextSessionId).ConfigureAwait(
false);
7146 await SendCommand<object>(DevToolsMethods.PageDisable,
new { }, nextSessionId).ConfigureAwait(
false);
7149 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"NextWindow: Switched to window index [{nextWindowIndex}], Context [{nextWindowContextId}], Tab ID [{nextTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
7150 return nextTargetId;
7159 public async Task<bool>
Normal(
string sessionId =
null)
7164 await SendCommand(DevToolsMethods.BrowserSetWindowBounds,
new
7166 windowId = windowId,
7167 bounds = new { windowState =
"normal" }
7168 }, sessionId).ConfigureAwait(
false);
7182 public async Task<string> OpenWindow(
string url =
"https://google.com",
string sessionId =
null)
7184 sessionId ??= GetEffectiveSessionId();
7187 var context = await SendCommand<object>(DevToolsMethods.TargetCreateBrowserContext,
new { }, sessionId).ConfigureAwait(
false);
7188 string browserContextId = context.browserContextId.ToString();
7190 var result = await SendCommand<object>(DevToolsMethods.TargetCreateTarget,
7191 new { url, browserContextId },
null).ConfigureAwait(
false);
7193 string targetId = result.targetId.ToString();
7196 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId },
null).ConfigureAwait(
false);
7197 var session = await SendCommand<object>(DevToolsMethods.TargetAttachToTarget,
7198 new { targetId, flatten = true },
null).ConfigureAwait(
false);
7200 string newSessionId = session.sessionId.ToString();
7203 var tabQueue =
new ConcurrentQueue<KeyValuePair<string, string>>();
7204 tabQueue.Enqueue(
new KeyValuePair<string, string>(targetId, newSessionId));
7207 _windowSessionsQueue.Enqueue(
new WindowSession(browserContextId, tabQueue, 0));
7210 SetActiveWindowIndex(_windowSessionsQueue.Count - 1);
7212 ((PuppeteerClient)PuppeteerClient)._currentTargetId = targetId;
7215 $
"Opened new window ContextId [{browserContextId}], TargetId [{targetId}], SessionId [{newSessionId}]",
7216 this, GPALObjectType.PuppeteerCommunicator);
7228 public async Task OverrideReferrer(
string url,
string sessionId =
null)
7230 sessionId ??= GetEffectiveSessionId();
7231 await SendCommand<object>(DevToolsMethods.NetworkSetExtraHTTPHeaders,
new { headers = new { Referer = url } }, sessionId).ConfigureAwait(
false);
7242 public async Task<dynamic> SetUserAgent(
string userAgent,
string sessionId =
null)
7244 sessionId ??= GetEffectiveSessionId();
7245 return await SendCommand<object>(DevToolsMethods.NetworkSetUserAgentOverride,
new { userAgent }, sessionId).ConfigureAwait(
false);
7257 public async Task PageDown(
int pagesToScroll = 1,
string sessionId =
null)
7259 sessionId ??= GetEffectiveSessionId();
7260 for (
int i = 0; i < pagesToScroll; i++)
7262 await ScrollPageAsync(
"down", pagesToScroll, sessionId).ConfigureAwait(
false);
7273 public async Task
PageEnd(
string sessionId =
null)
7286 public async Task PageTop(
string sessionId =
null)
7288 sessionId ??= GetEffectiveSessionId();
7289 await ScrollToPositionAsync(
"top", sessionId).ConfigureAwait(
false);
7300 public async Task PageUp(
int pagesToScroll = 1,
string sessionId =
null)
7302 sessionId ??= GetEffectiveSessionId();
7303 for (
int i = 0; i < pagesToScroll; i++)
7305 await ScrollPageAsync(
"up", pagesToScroll, sessionId).ConfigureAwait(
false);
7318 public async Task PressModifierKey(
int modifierKeys,
string sessionId =
null)
7320 sessionId ??= GetEffectiveSessionId();
7321 await SendCommand<object>(DevToolsMethods.InputDispatchKeyEvent,
new { type =
"keyDown", modifiers = modifierKeys }, sessionId).ConfigureAwait(
false);
7333 public async Task<string> PreviousTab(
string currentTargetId)
7335 var windowArray = _windowSessionsQueue.ToArray();
7336 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
7338 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"No active window for PreviousTab",
this, GPALObjectType.PuppeteerCommunicator);
7342 var session = windowArray[_activeWindowIndex];
7343 var tabQueue = session.TabQueue;
7344 var browserContextId = session.BrowserContextId;
7347 var targetsResponse = await SendCommand<dynamic>(
7348 DevToolsMethods.TargetGetTargets,
7351 ).ConfigureAwait(
false);
7353 if (targetsResponse?.targetInfos ==
null)
7355 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"Failed to get targets list in NextTab sync",
this, GPALObjectType.PuppeteerCommunicator);
7360 var knownTargetIds =
new HashSet<string>(tabQueue.Select(kv => kv.Key));
7362 foreach (var targetInfo
in targetsResponse.targetInfos)
7364 string targetId = targetInfo.targetId?.ToString();
7365 string type = targetInfo.type?.ToString();
7366 string contextId = targetInfo.browserContextId?.ToString();
7368 if (
string.IsNullOrEmpty(targetId) || type !=
"page" || contextId != browserContextId)
7371 if (!knownTargetIds.Contains(targetId))
7374 await AddTabToQueue(targetId,
null,
false,
true).ConfigureAwait(
false);
7375 knownTargetIds.Add(targetId);
7380 var queueArray = tabQueue.ToArray();
7381 if (queueArray.Length == 0)
7383 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"No tabs for PreviousTab",
this, GPALObjectType.PuppeteerCommunicator);
7387 var currentIndex = Array.FindIndex(queueArray, kvp => kvp.Key == currentTargetId);
7388 if (currentIndex == -1)
7390 currentIndex = windowArray[_activeWindowIndex].ActiveTabIndex;
7393 var prevIndex = (currentIndex - 1 + queueArray.Length) % queueArray.Length;
7394 var prevTargetId = queueArray[prevIndex].Key;
7395 var prevSessionId = queueArray[prevIndex].Value;
7398 var windowList = _windowSessionsQueue.ToList();
7400 windowList[_activeWindowIndex].BrowserContextId,
7404 _windowSessionsQueue =
new ConcurrentQueue<WindowSession>();
7405 foreach (var w
in windowList)
7407 _windowSessionsQueue.Enqueue(w);
7412 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId = prevTargetId },
null).ConfigureAwait(
false);
7414 if (await SendCommand<object>(DevToolsMethods.PageEnable,
new { }, prevSessionId).ConfigureAwait(
false) ==
null)
7416 var newSession = await SendCommand<object>(DevToolsMethods.TargetAttachToTarget,
new { targetId = prevTargetId, flatten = true },
null).ConfigureAwait(
false);
7417 if (newSession?.sessionId !=
null)
7419 prevSessionId = (string)newSession.sessionId;
7420 RemoveTabFromQueue(prevTargetId);
7421 await AddTabToQueue(prevTargetId, prevSessionId).ConfigureAwait(
false);
7425 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to re-attach PreviousTab ID [{prevTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
7428 await SendCommand<object>(DevToolsMethods.PageDisable,
new { }, prevSessionId).ConfigureAwait(
false);
7432 await SendCommand<object>(DevToolsMethods.PageDisable,
new { }, prevSessionId).ConfigureAwait(
false);
7435 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"PreviousTab: Switched to index [{prevIndex}], ID [{prevTargetId}], Active window tab queue size [{queueArray.Length}]",
this, GPALObjectType.PuppeteerCommunicator);
7436 return prevTargetId;
7449 var windowArray = _windowSessionsQueue.ToArray();
7450 if (windowArray.Length == 0)
7452 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No windows for PreviousWindow",
this, GPALObjectType.PuppeteerCommunicator);
7456 var currentWindowIndex = -1;
7457 for (
int i = 0; i < windowArray.Length; i++)
7459 var tabArray = windowArray[i].TabQueue.ToArray();
7460 if (Array.Exists(tabArray, kvp => kvp.Key == currentTargetId))
7462 currentWindowIndex = i;
7466 if (currentWindowIndex == -1)
7468 currentWindowIndex = _activeWindowIndex;
7471 var prevWindowIndex = (currentWindowIndex - 1 + windowArray.Length) % windowArray.Length;
7472 SetActiveWindowIndex(prevWindowIndex);
7474 var prevWindowContextId = windowArray[prevWindowIndex].BrowserContextId;
7475 var prevTabQueue = windowArray[prevWindowIndex].TabQueue;
7476 var prevTabArray = prevTabQueue.ToArray();
7477 if (prevTabArray.Length == 0)
7479 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"No tabs in previous window for PreviousWindow (context [{prevWindowContextId}])",
this, GPALObjectType.PuppeteerCommunicator);
7483 var prevTargetId = prevTabArray[windowArray[prevWindowIndex].ActiveTabIndex].Key;
7484 var prevSessionId = prevTabArray[windowArray[prevWindowIndex].ActiveTabIndex].Value;
7486 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId = prevTargetId },
null).ConfigureAwait(
false);
7488 if (await SendCommand<object>(DevToolsMethods.PageEnable,
new { }, prevSessionId).ConfigureAwait(
false) ==
null)
7490 var newSession = await SendCommand<object>(DevToolsMethods.TargetAttachToTarget,
new { targetId = prevTargetId, flatten = true },
null).ConfigureAwait(
false);
7491 if (newSession?.sessionId !=
null)
7493 prevSessionId = (string)newSession.sessionId;
7494 RemoveTabFromQueue(prevTargetId);
7495 await AddTabToQueue(prevTargetId, prevSessionId).ConfigureAwait(
false);
7499 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to re-attach PreviousWindow tab ID [{prevTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
7502 await SendCommand<object>(DevToolsMethods.PageDisable,
new { }, prevSessionId).ConfigureAwait(
false);
7506 await SendCommand<object>(DevToolsMethods.PageDisable,
new { }, prevSessionId).ConfigureAwait(
false);
7509 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"PreviousWindow: Switched to window index [{prevWindowIndex}], Context [{prevWindowContextId}], Tab ID [{prevTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
7510 return prevTargetId;
7519 public async Task<dynamic> QuerySelector(
string css,
string sessionId =
null)
7521 sessionId ??= GetEffectiveSessionId();
7532 internal async Task<long> DocumentMark(
string sessionId =
null)
7534 sessionId ??= GetEffectiveSessionId();
7536 var doc = await SendCommand<object>(DevToolsMethods.DOMGetDocument,
new { depth = 0 }, sessionId).ConfigureAwait(
false);
7537 long mark =
null == doc ||
null == doc.root ? 0 : (long)doc.root.backendNodeId;
7550 public async Task<dynamic> QuerySelectors(
string css,
string sessionId =
null)
7552 sessionId ??= GetEffectiveSessionId();
7554 var doc = await SendCommand<object>(DevToolsMethods.DOMGetDocument,
new { depth = 0 }, sessionId).ConfigureAwait(
false);
7556 return await SendCommand<object>(DevToolsMethods.DOMQuerySelectorAll,
new { nodeId = (int)doc.root.nodeId, selector = css }, sessionId).ConfigureAwait(
false);
7566 public async Task Refresh(
string sessionId =
null)
7568 sessionId ??= GetEffectiveSessionId();
7569 await SendCommand<object>(DevToolsMethods.PageReload,
new { }, sessionId).ConfigureAwait(
false);
7581 public async Task ReleaseModifierKey(
int modifierKeys,
string sessionId =
null)
7583 sessionId ??= GetEffectiveSessionId();
7584 await SendCommand<object>(DevToolsMethods.InputDispatchKeyEvent,
new { type =
"keyUp", modifiers = modifierKeys }, sessionId).ConfigureAwait(
false);
7594 public async Task<bool> Restore(
string sessionId =
null)
7596 sessionId ??= GetEffectiveSessionId();
7598 var windowId = await GetCurrentWindow(sessionId).ConfigureAwait(
false);
7599 await SendCommand(DevToolsMethods.BrowserSetWindowBounds,
new
7601 windowId = windowId,
7602 bounds = new { windowState =
"normal" }
7603 }, sessionId).ConfigureAwait(
false);
7622 public async Task MiddleClick(
string elementId,
string sessionId =
null)
7624 sessionId ??= GetEffectiveSessionId();
7625 List<GPALElement> elems = await EvaluateSelector(elementId, sessionId).ConfigureAwait(
false);
7626 List<dynamic> responses =
new List<dynamic>();
7628 foreach (GPALElement elem
in elems)
7630 await ScrollIntoView(elem, sessionId).ConfigureAwait(
false);
7631 await ClickElement(elem, sessionId, responses, ClickType.MiddleClick).ConfigureAwait(
false);
7649 public async Task RightClick(
string elementId,
string sessionId =
null)
7651 sessionId ??= GetEffectiveSessionId();
7652 List<GPALElement> elems = await EvaluateSelector(elementId, sessionId).ConfigureAwait(
false);
7653 List<dynamic> responses =
new List<dynamic>();
7655 foreach (GPALElement elem
in elems)
7657 await ScrollIntoView(elem, sessionId).ConfigureAwait(
false);
7658 await ClickElement(elem, sessionId, responses, ClickType.RightClick).ConfigureAwait(
false);
7674 public async Task ScrollElement(
string elementId,
int hPixels,
int vPixels,
string sessionId =
null)
7676 sessionId ??= GetEffectiveSessionId();
7677 await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
7679 contextId = CurrentContextId,
7680 expression =
"document.getElementById('" + elementId +
"').scrollBy(" + hPixels +
", " + vPixels +
")"
7681 }, sessionId).ConfigureAwait(
false);
7701 public async Task<bool> ScrollIntoView(GPALElement element,
string sessionId =
null)
7703 int backendNodeId = element.ElementBackendNodeId;
7705 sessionId ??= GetEffectiveSessionId();
7709 if (
"GPALElement".Equals(element.TagName))
7713 if (
true == Browser.BrowserSettings.UseHeadless)
7716 await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
7718 contextId = CurrentContextId,
7720 document.body.style.zoom = '100%'; // minor reflow trigger
7721 window.dispatchEvent(new Event('resize'));
7722 document.documentElement.scrollTop += 1; document.documentElement.scrollTop -= 1;"
7723 }, sessionId).ConfigureAwait(
false);
7727 await SendCommand(DevToolsMethods.RuntimeEvaluate,
new
7730 if (!document.querySelector('style[data-gpal-overscroll]')) {
7731 const style = document.createElement('style');
7732 style.setAttribute('data-gpal-overscroll', 'true');
7733 style.textContent = `
7735 overscroll-behavior: none !important;
7736 overscroll-behavior-x: none !important;
7737 overscroll-behavior-y: none !important;
7740 (document.head || document.documentElement).appendChild(style);
7743 returnByValue = true
7744 }, sessionId).ConfigureAwait(
false);
7747 var scrollResult = await SendCommand<object>(
7748 DevToolsMethods.DOMScrollIntoViewIfNeeded,
7749 new { backendNodeId },
7751 ).ConfigureAwait(
false);
7753 if (
true == Browser.BrowserSettings.UseHeadless)
7754 await Task.Delay(250).ConfigureAwait(
false);
7756 if (
null == scrollResult)
7759 var escapedSelector = element.Css.Replace(
@"\",
@"\\").Replace(
"'",
@"\'");
7761 var callResult = await SendCommand<dynamic>(
7762 DevToolsMethods.RuntimeEvaluate,
7767 const el = document.querySelector(""{escapedSelector}"");
7769 console.warn('Element not found by selector: {escapedSelector}');
7773 el.scrollIntoView({{
7774 behavior: 'instant',
7779 // Force reflow/layout
7780 void el.getBoundingClientRect();
7781 void el.offsetHeight;
7783 return new Promise(r => requestAnimationFrame(() => r(true)));
7785 awaitPromise = true,
7786 contextId = CurrentContextId,
7787 returnByValue = true
7790 ).ConfigureAwait(
false);
7808 catch (Exception ex)
7811 $
"ScrollIntoView failed",
7812 this, GPALObjectType.PuppeteerCommunicator, ex);
7909 public async Task
ScrollWindow(
int hPixels,
int vPixels,
string sessionId =
null)
7913 await Task.Delay(150).ConfigureAwait(
false);
7928 public async Task<dynamic> SendKey(
string key,
string code,
int vkCode,
string sessionId =
null)
7930 sessionId ??= GetEffectiveSessionId();
7936 string textPayload = (code ==
"Enter") ?
"\r" : (key.Length == 1 ? key : null);
7939 var keyDownParams =
new
7941 type =
"rawKeyDown",
7944 windowsVirtualKeyCode = vkCode,
7945 nativeVirtualKeyCode = vkCode
7947 await SendCommand<object>(DevToolsMethods.InputDispatchKeyEvent, keyDownParams, sessionId).ConfigureAwait(
false);
7950 if (textPayload !=
null)
7952 var charParams =
new
7956 unmodifiedText = textPayload,
7959 windowsVirtualKeyCode = vkCode,
7960 nativeVirtualKeyCode = vkCode
7962 await SendCommand<object>(DevToolsMethods.InputDispatchKeyEvent, charParams, sessionId).ConfigureAwait(
false);
7966 var keyUpParams =
new
7971 windowsVirtualKeyCode = vkCode,
7972 nativeVirtualKeyCode = vkCode
7974 await SendCommand<dynamic>(DevToolsMethods.InputDispatchKeyEvent, keyUpParams, sessionId).ConfigureAwait(
false);
7976 catch (Exception ex)
7978 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"SendKey execution failed for Code [{code}]",
this, GPALObjectType.PuppeteerCommunicator, ex);
7990 private int GetVirtualKeyCode(
string key)
7992 if (
string.IsNullOrEmpty(key))
7995 string k = key.ToLowerInvariant();
8051 case "f1":
return 0x70;
8052 case "f2":
return 0x71;
8053 case "f3":
return 0x72;
8054 case "f4":
return 0x73;
8055 case "f5":
return 0x74;
8056 case "f6":
return 0x75;
8057 case "f7":
return 0x76;
8058 case "f8":
return 0x77;
8059 case "f9":
return 0x78;
8060 case "f10":
return 0x79;
8061 case "f11":
return 0x7A;
8062 case "f12":
return 0x7B;
8082 public async Task SendString(
string text,
int delayMs = 0,
string sessionId =
null)
8084 sessionId ??= GetEffectiveSessionId();
8086 await SendCommand<object>(DevToolsMethods.InputInsertText,
new { text }, sessionId).ConfigureAwait(
false);
8088 await DispatchTextCharByChar(text, delayMs, sessionId).ConfigureAwait(
false);
8100 private async Task DispatchTextCharByChar(
string text,
int delayMs,
string sessionId)
8103 foreach (
char c
in text)
8105 if (
false == first && 0 < delayMs)
8106 await Task.Delay(HardwareHelper.GetTypingDelay(delayMs)).ConfigureAwait(
false);
8108 string key = c.ToString();
8109 int vkCode = (int)c;
8111 await SendCommand(DevToolsMethods.InputDispatchKeyEvent,
new
8115 unmodifiedtext = key,
8117 code = GetKeyCode(c),
8118 windowsVirtualKeyCode = vkCode,
8119 nativeVirtualKeyCode = vkCode
8120 }, sessionId).ConfigureAwait(
false);
8122 await SendCommand(DevToolsMethods.InputDispatchKeyEvent,
new
8126 code = GetKeyCode(c),
8127 windowsVirtualKeyCode = vkCode
8128 }, sessionId).ConfigureAwait(
false);
8144 public async Task<dynamic> SetAttribute(
int backendNodeId,
string attribute,
string value,
string sessionId =
null)
8146 sessionId ??= GetEffectiveSessionId();
8147 var nodeId = await GetNodeIdFromBackendNodeId(backendNodeId, sessionId).ConfigureAwait(
false);
8148 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Set attribute [{attribute}] to [{value}] on backendNodId [{backendNodeId}]",
this, GPALObjectType.PuppeteerCommunicator);
8149 return await SendCommand<object>(DevToolsMethods.DOMSetAttributeValue,
new { nodeId, name = attribute, value }, sessionId).ConfigureAwait(
false);
8160 public async Task SetDownloadFilename(
string downloadPath,
string sessionId =
null)
8162 sessionId ??= GetEffectiveSessionId();
8163 await SendCommand<object>(DevToolsMethods.PageSetDownloadBehavior,
new { behavior =
"allow", downloadPath }, sessionId).ConfigureAwait(
false);
8176 public async Task<dynamic> SetRange(
string elementId,
string rangeValue,
string sessionId =
null)
8178 sessionId ??= GetEffectiveSessionId();
8179 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8182 contextId = CurrentContextId,
8183 expression =
$@"var s = document.querySelector('{elementId}'); s.value = {rangeValue}; s.dispatchEvent(new Event('input', {{bubbles: true}})); s.dispatchEvent(new Event('change', {{bubbles: true}}));"
8184 }, sessionId).ConfigureAwait(
false);
8194 public async Task TopBrowser(
string sessionId =
null)
8199 await SendCommand<object>(DevToolsMethods.PageBringToFront,
new { }, GetCurrentSessionId()).ConfigureAwait(
false);
8214 public async Task<bool> SetStorage(
8220 string storeName =
null,
8223 bool retVal =
false;
8224 sessionId ??= GetEffectiveSessionId();
8228 switch (storageType)
8232 IDictionary<string, object> cookieToSet;
8238 dynamic cookiesResp = await
GetStorage(storageType, sessionId, domain, storeName, path, key).ConfigureAwait(
false);
8241 dynamic existing =
null;
8243 if (cookiesResp !=
null)
8245 var cookies = JsonConvert.DeserializeObject<List<dynamic>>(cookiesResp) ??
new List<dynamic>();
8246 foreach (dynamic c
in cookies)
8248 string cDomain = ((dynamic)c).Domain ??
"";
8249 string cPath = ((dynamic)c).Path ??
"/";
8250 string cName = ((dynamic)c).Key ??
"";
8253 UrlHelper.CookieDomainMatches(cDomain, domain) &&
8258 cookieToSet =
new System.Dynamic.ExpandoObject() as IDictionary<string, object>;
8260 cookieToSet[
"name"] = key;
8261 cookieToSet[
"value"] = data;
8262 cookieToSet[
"domain"] = ((dynamic)existing)?.Domain?.ToString();
8263 cookieToSet[
"path"] = path;
8265 cookieToSet[
"expires"] = ((dynamic)existing)?.Expires !=
null
8266 ? (
double)existing.Expires
8267 : DateTimeOffset.UtcNow.ToUnixTimeSeconds() + (180L * 24 * 60 * 60);
8269 cookieToSet[
"secure"] = ((dynamic)existing)?.Secure ??
true;
8270 cookieToSet[
"httpOnly"] = ((dynamic)existing)?.HttpOnly ??
true;
8271 cookieToSet[
"sameSite"] = ((dynamic)existing)?.SameSite?.ToString() ??
"None";
8273 cookieToSet[
"session"] = ((dynamic)existing)?.Session ??
false;
8274 cookieToSet[
"priority"] = ((dynamic)existing)?.Priority?.ToString() ??
"Medium";
8276 cookieToSet[
"sourceScheme"] = ((dynamic)existing)?.SourceScheme?.ToString() ??
"Secure";
8277 cookieToSet[
"sourcePort"] = ((dynamic)existing)?.SourcePort ?? 443;
8280 if (((dynamic)existing)?.PartitionKey !=
null)
8282 cookieToSet[
"partitionKey"] = existing.PartitionKey;
8286 await SendCommand<dynamic>(DevToolsMethods.StorageSetCookies,
8287 new { cookies = new[] { cookieToSet } },
8288 sessionId).ConfigureAwait(
false);
8295 cookieToSet =
new System.Dynamic.ExpandoObject() as IDictionary<string, object>;
8297 cookieToSet[
"name"] = key;
8298 cookieToSet[
"value"] = data;
8299 cookieToSet[
"domain"] = ((dynamic)existing)?.Domain?.ToString();
8300 cookieToSet[
"path"] = path;
8302 cookieToSet[
"expires"] = ((dynamic)existing)?.Expires !=
null
8303 ? (
double)existing.Expires
8304 : DateTimeOffset.UtcNow.ToUnixTimeSeconds() + (180L * 24 * 60 * 60);
8306 cookieToSet[
"secure"] = ((dynamic)existing)?.Secure ??
true;
8307 cookieToSet[
"httpOnly"] = ((dynamic)existing)?.HttpOnly ??
true;
8308 cookieToSet[
"sameSite"] = ((dynamic)existing)?.SameSite?.ToString() ??
"None";
8310 cookieToSet[
"session"] = ((dynamic)existing)?.Session ??
false;
8311 cookieToSet[
"priority"] = ((dynamic)existing)?.Priority?.ToString() ??
"Medium";
8313 cookieToSet[
"sourceScheme"] = ((dynamic)existing)?.SourceScheme?.ToString() ??
"Secure";
8314 cookieToSet[
"sourcePort"] = ((dynamic)existing)?.SourcePort ?? 443;
8317 if (((dynamic)existing)?.PartitionKey !=
null)
8319 cookieToSet[
"partitionKey"] = existing.PartitionKey;
8323 await SendCommand<dynamic>(DevToolsMethods.StorageSetCookies,
8324 new { cookies = new[] { cookieToSet } },
8325 sessionId).ConfigureAwait(
false);
8334 case "localStorage":
8335 case "sessionStorage":
8336 string jsStorage =
$@"{storageType}.setItem('{EscapeJsString(key)}','{EscapeJsString(data)}');";
8337 await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
8339 expression = jsStorage,
8340 awaitPromise = false,
8341 contextId = CurrentContextId,
8342 returnByValue = true
8343 }, sessionId).ConfigureAwait(
false);
8352 new Promise((callback) => {{
8355 const dbName = '{EscapeJsString(path)}';
8356 const storeName = '{EscapeJsString(storeName)}';
8357 const inputKey = '{EscapeJsString(key)}';
8358 let rawValue = '{EscapeJsString(data)}';
8360 function resolve(val) {{
8364 function fail(msg, err, db) {{
8365 console.error(msg, err?.name || err || '');
8366 try {{ db?.close(); }} catch {{ }}
8370 function parseValue(v) {{
8372 return JSON.parse(v);
8378 function isValidKey(k) {{
8379 if (k === undefined) return false;
8380 if (typeof k === 'string' || typeof k === 'number') return true;
8381 if (k instanceof Date) return true;
8382 if (Array.isArray(k)) return true;
8386 function ensureInlineKey(value, keyPath, key, autoIncrement) {{
8387 if (keyPath === null) return {{ value, usedKey: key }};
8389 if (typeof value !== 'object' || value === null) {{
8390 if (keyPath && key !== undefined) {{
8391 const wrapped = {{}};
8393 if (typeof keyPath === 'string') {{
8394 wrapped[keyPath] = key;
8395 }} else if (Array.isArray(keyPath)) {{
8396 if (!Array.isArray(key)) {{
8397 throw new Error('Compound key requires array key');
8399 for (let i = 0; i < keyPath.length; i++) {{
8400 wrapped[keyPath[i]] = key[i];
8404 wrapped.value = value;
8405 return {{ value: wrapped, usedKey: undefined }};
8408 if (autoIncrement) {{
8409 return {{ value, usedKey: undefined }};
8412 throw new Error('Inline keyPath requires object value');
8415 if (typeof keyPath === 'string') {{
8416 if (!(keyPath in value)) {{
8417 if (key !== undefined) {{
8418 value[keyPath] = key;
8419 }} else if (!autoIncrement) {{
8420 throw new Error('Missing required inline keyPath property');
8423 }} else if (Array.isArray(keyPath)) {{
8424 for (let i = 0; i < keyPath.length; i++) {{
8425 const kp = keyPath[i];
8426 if (!(kp in value)) {{
8427 if (Array.isArray(key) && key[i] !== undefined) {{
8429 }} else if (!autoIncrement) {{
8430 throw new Error('Missing compound keyPath property: ' + kp);
8436 return {{ value, usedKey: undefined }};
8439 const openReq = indexedDB.open(dbName);
8441 openReq.onerror = () => {{
8442 fail('Failed to open database', openReq.error);
8445 openReq.onsuccess = () => {{
8446 const db = openReq.result;
8450 tx = db.transaction(storeName, 'readwrite');
8452 fail('Transaction creation failed', e, db);
8456 const store = tx.objectStore(storeName);
8458 let value = parseValue(rawValue);
8461 if (key === '') key = undefined;
8463 if (key !== undefined && !isValidKey(key)) {{
8464 fail('Invalid key type', key, db);
8468 let finalValue, finalKey;
8471 const result = ensureInlineKey(
8478 finalValue = result.value;
8479 finalKey = result.usedKey;
8481 fail('Key handling failed', e, db);
8488 if (store.keyPath === null) {{
8489 if (finalKey === undefined && !store.autoIncrement) {{
8490 fail('Missing key for out-of-line store', null, db);
8493 putReq = store.put(finalValue, finalKey);
8495 putReq = store.put(finalValue);
8498 fail('Put setup error', e, db);
8502 // wait for transaction complete (not just put)
8503 tx.oncomplete = () => {{
8504 try {{ db.close(); }} catch {{ }}
8508 tx.onerror = (event) => {{
8509 fail('Transaction error', event.target.error, db);
8512 tx.onabort = (event) => {{
8513 fail('Transaction aborted', event.target.error, db);
8521 string indexedDbSaid = SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
8524 awaitPromise = true,
8525 contextId = CurrentContextId,
8526 returnByValue = true
8527 }, sessionId).GetAwaiter().GetResult()?.ToString();
8529 retVal =
true == indexedDbSaid?.Contains(
"true");
8533 if (
false == retVal)
8535 $
"IndexedDB [{path}] store [{storeName}] did not take key [{key}], the page answered [{indexedDbSaid}]",
8536 null, GPALObjectType.None);
8541 string cacheName = EscapeJsString(storeName);
8542 string requestUrl = EscapeJsString(key);
8547 const cache = await caches.open('{cacheName}');
8549 const request = new Request('{requestUrl}', {{
8553 const response = new Response('{EscapeJsString(data)}', {{
8557 'Content-Type': 'text/plain;charset=utf-8',
8558 'Cache-Control': 'max-age=31536000' // or make this configurable
8562 await cache.put(request, response);
8563 return {{ success: true, cacheName: '{cacheName}', key: '{requestUrl}' }};
8565 console.error('Cache set failed:', err);
8566 return {{ success: false, error: err.message }};
8571 var evalResult = await SendCommand<dynamic>(
8572 DevToolsMethods.RuntimeEvaluate,
8576 awaitPromise = true,
8577 contextId = CurrentContextId,
8578 returnByValue = true
8580 sessionId).ConfigureAwait(
false);
8583 retVal = evalResult?.result?.value?.success ==
true;
8588 GPALEventType.ERROR,
8589 $
"Unsupported storage type: [{storageType}]",
8591 GPALObjectType.PuppeteerCommunicator);
8595 catch (Exception ex)
8598 GPALEventType.EXCEPTION,
8599 $
"SetStorage failed for [{storageType}] domain=[{domain}] storeName=[{storeName}] path=[{path}] key=[{key}]",
8601 GPALObjectType.PuppeteerCommunicator,
8619 public async Task<dynamic> SetValueFromElement(
string srcCss,
string destCss,
string sessionId =
null)
8621 sessionId ??= GetEffectiveSessionId();
8622 return await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8625 contextId = CurrentContextId,
8626 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}}));"
8627 }, sessionId).ConfigureAwait(
false);
8638 public async Task SetGoogleReferrer(
string sessionId =
null)
8640 sessionId ??= GetEffectiveSessionId();
8641 await SendCommand<object>(DevToolsMethods.NetworkSetExtraHTTPHeaders,
new { headers = new { Referer =
"https://www.google.com" } }, sessionId).ConfigureAwait(
false);
8651 public Task SwitchToDefaultContent(
string sessionId =
null)
8655 CurrentContextId =
null;
8656 CurrentFrameId =
null;
8657 CurrentFrameSessionId =
null;
8658 CurrentRootObjectId =
null;
8660 return Task.CompletedTask;
8670 public async Task<string>
GoToWindow(
object tabIdOrUrl,
string sessionId =
null)
8672 string targetId =
null;
8674 if (tabIdOrUrl is
int tabId)
8677 if (tabId >= 0 && tabId < queueArray.Length)
8679 targetId = queueArray[tabId].Key;
8681 SetActiveTabIndex(tabId);
8684 else if (tabIdOrUrl is
string url && !
string.IsNullOrWhiteSpace(url))
8687 if (!
string.IsNullOrEmpty(targetId))
8691 if (windowIndex >= 0)
8693 SetActiveWindowIndex(windowIndex);
8694 var windowArray = _windowSessionsQueue.ToArray();
8695 var tabQueue = windowArray[windowIndex].TabQueue.ToArray();
8696 int tabIndex = Array.FindIndex(tabQueue, kvp => kvp.Key == targetId);
8699 SetActiveTabIndex(tabIndex);
8705 if (
string.IsNullOrEmpty(targetId))
8707 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No target found for tabIdOrUrl [{tabIdOrUrl}]",
this, GPALObjectType.PuppeteerCommunicator);
8711 await SendCommand<object>(DevToolsMethods.TargetActivateTarget,
new { targetId },
null).ConfigureAwait(
false);
8712 ((PuppeteerClient)PuppeteerClient)._currentTargetId = targetId;
8721 public async Task<int> WindowInnerHeight(
string sessionId =
null)
8723 sessionId ??= GetEffectiveSessionId();
8724 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8726 expression =
"window.innerHeight",
8727 contextId = CurrentContextId,
8728 returnByValue = true
8729 }, sessionId).ConfigureAwait(
false);
8730 return (
int)result.value;
8739 public async Task<int> WindowInnerWidth(
string sessionId =
null)
8741 sessionId ??= GetEffectiveSessionId();
8742 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8744 expression =
"window.innerWidth",
8745 contextId = CurrentContextId,
8746 returnByValue = true
8747 }, sessionId).ConfigureAwait(
false);
8748 return (
int)result.value;
8757 public async Task<int> WindowOuterHeight(
string sessionId =
null)
8759 sessionId ??= GetEffectiveSessionId();
8760 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8762 expression =
"window.outerHeight",
8763 contextId = CurrentContextId,
8764 returnByValue = true
8765 }, sessionId).ConfigureAwait(
false);
8766 return (
int)result.value;
8775 public async Task<int> WindowOuterWidth(
string sessionId =
null)
8777 sessionId ??= GetEffectiveSessionId();
8778 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8780 expression =
"window.outerWidth",
8781 contextId = CurrentContextId,
8782 returnByValue = true
8783 }, sessionId).ConfigureAwait(
false);
8784 return (
int)result.value;
8801 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)
8803 sessionId ??= GetEffectiveSessionId();
8805 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8810 {BrowserHelper.FetchSetupScript(url, method, body, contentType, headers, asBytes)}
8813 var response = await fetch(target.toString(), options);
8814 return await gpalEnvelope(response);
8816 return gpalFailure(error);
8819 contextId = CurrentContextId,
8820 returnByValue = true,
8822 }, sessionId).ConfigureAwait(
false);
8828 Newtonsoft.Json.Linq.JObject evaluated = result as Newtonsoft.Json.Linq.JObject;
8829 Newtonsoft.Json.Linq.JToken value = evaluated?[
"result"]?[
"value"];
8834 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"The page did not answer the fetch. Runtime.evaluate returned [{(null == evaluated ? "nothing
" : Newtonsoft.Json.JsonConvert.SerializeObject(evaluated))}]",
null, GPALObjectType.PuppeteerCommunicator);
8836 return value?.ToString();
8845 public async Task<int> WindowPageOffsetX(
string sessionId =
null)
8847 sessionId ??= GetEffectiveSessionId();
8848 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8850 expression =
"window.pageXOffset",
8851 contextId = CurrentContextId,
8852 returnByValue = true
8853 }, sessionId).ConfigureAwait(
false);
8854 return (
int)result.value;
8863 public async Task<int> WindowPageOffsetY(
string sessionId =
null)
8865 sessionId ??= GetEffectiveSessionId();
8866 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8868 expression =
"window.pageYOffset",
8869 contextId = CurrentContextId,
8870 returnByValue = true
8871 }, sessionId).ConfigureAwait(
false);
8872 return (
int)result.value;
8881 public async Task<int> WindowScreenLeft(
string sessionId =
null)
8883 sessionId ??= GetEffectiveSessionId();
8884 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8886 expression =
"window.screenLeft",
8887 contextId = CurrentContextId,
8888 returnByValue = true
8889 }, sessionId).ConfigureAwait(
false);
8890 return (
int)result.value;
8899 public async Task<int> WindowScreenTop(
string sessionId =
null)
8901 sessionId ??= GetEffectiveSessionId();
8902 var result = await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
8904 expression =
"window.screenTop",
8905 contextId = CurrentContextId,
8906 returnByValue = true
8907 }, sessionId).ConfigureAwait(
false);
8908 return (
int)result.value;
8919 private async Task<int> GetNodeIdFromBackendNodeId(
int backendNodeId,
string sessionId =
null)
8921 sessionId ??= GetEffectiveSessionId();
8924 await SendCommand<dynamic>(DevToolsMethods.DOMGetDocument,
new { depth = 1 }, sessionId).ConfigureAwait(
false);
8927 var result = await SendCommand<dynamic>(
8928 DevToolsMethods.DOMPushNodesByBackendIdsToFrontend,
8929 new { backendNodeIds = new[] { backendNodeId } },
8931 ).ConfigureAwait(
false);
8934 if (result?.nodeIds is Newtonsoft.Json.Linq.JArray nodeIdsArray && nodeIdsArray.Count > 0)
8936 return nodeIdsArray[0].Value<
int>();
8944 public async Task<string> GetObjectIdFromBackendNodeId(
int backendNodeId,
string sessionId =
null,
long? contextId =
null)
8946 sessionId ??= GetEffectiveSessionId();
8949 string effectiveSessionId = !
string.IsNullOrEmpty(CurrentFrameSessionId)
8950 ? CurrentFrameSessionId
8954 long? effectiveContextId = contextId ?? CurrentContextId;
8959 var resolveNodeParams =
new Dictionary<string, object>
8961 [
"backendNodeId"] = backendNodeId
8965 if (effectiveContextId.HasValue)
8967 resolveNodeParams[
"executionContextId"] = effectiveContextId.Value;
8970 var resolveResult = await SendCommand<dynamic>(DevToolsMethods.DOMResolveNode, resolveNodeParams, effectiveSessionId).ConfigureAwait(
false);
8972 string objectId = resolveResult?.@
object?.objectId?.ToString();
8974 if (!
string.IsNullOrEmpty(objectId))
8978 var fallbackResult = await SendCommand<dynamic>(DevToolsMethods.DOMResolveNode,
new
8980 backendNodeId = backendNodeId
8981 }, effectiveSessionId).ConfigureAwait(
false);
8983 return fallbackResult?.@
object?.objectId?.ToString() ??
string.Empty;
8985 catch (Exception ex)
8987 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
8988 $
"GetObjectIdFromBackendNodeId failed for backendNodeId [{backendNodeId}]",
8989 this, GPALObjectType.PuppeteerCommunicator, ex);
8991 return string.Empty;
9003 private async Task<int> GetElementRandomCenterX(
int backendNodeId,
string sessionId =
null)
9005 sessionId ??= GetEffectiveSessionId();
9006 var rect = await GetBoundingClientRect(backendNodeId, sessionId).ConfigureAwait(
false);
9007 return (
int)rect.X + rect.Width /
new Random().Next(2, 3);
9018 private async Task<int> GetElementRandomCenterY(
int backendNodeId,
string sessionId =
null)
9020 sessionId ??= GetEffectiveSessionId();
9021 var rect = await GetBoundingClientRect(backendNodeId, sessionId).ConfigureAwait(
false);
9022 return (
int)rect.Y + rect.Width /
new Random().Next(2, 3);
9030 internal string GetProtocolJSON()
9032 using var http =
new System.Net.Http.HttpClient();
9033 var json = http.GetStringAsync($
"{_puppeteerUrl}/json/protocol").Result;
9034 return Newtonsoft.Json.Linq.JObject.Parse(json).ToString();
9040 internal string GetActiveWindowId()
9042 var windows = _windowSessionsQueue.ToArray();
9043 if (_activeWindowIndex < 0 || _activeWindowIndex >= windows.Length)
9045 return windows[_activeWindowIndex].BrowserContextId;
9052 private ConcurrentQueue<KeyValuePair<string, string>> GetActiveWindowTabQueue()
9054 var windowArray = _windowSessionsQueue.ToArray();
9055 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
return null;
9056 return windowArray[_activeWindowIndex].TabQueue;
9064 public string GetEffectiveSessionId()
9066 return !
string.IsNullOrEmpty(CurrentFrameSessionId)
9067 ? CurrentFrameSessionId
9068 : GetCurrentSessionId();
9084 internal void ClearCurrentSessionId()
9086 var windowArray = _windowSessionsQueue.ToArray();
9087 if (0 == windowArray.Length || _activeWindowIndex >= windowArray.Length)
9090 var window = windowArray[_activeWindowIndex];
9091 var tabArray = window.TabQueue.ToArray();
9092 if (0 == tabArray.Length || window.ActiveTabIndex >= tabArray.Length)
9095 string targetId = tabArray[window.ActiveTabIndex].Key;
9096 var newQueue =
new ConcurrentQueue<KeyValuePair<string, string>>();
9098 foreach (var kvp
in tabArray)
9099 newQueue.Enqueue(kvp.Key == targetId ?
new KeyValuePair<string, string>(kvp.Key,
null) : kvp);
9101 window.TabQueue = newQueue;
9103 public string GetCurrentSessionId()
9105 var windowArray = _windowSessionsQueue.ToArray();
9106 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
return null;
9108 var window = windowArray[_activeWindowIndex];
9109 var tabQueue = window.TabQueue;
9110 var tabArray = tabQueue.ToArray();
9111 var activeTabIndex = window.ActiveTabIndex;
9112 if (tabArray.Length == 0 || activeTabIndex >= tabArray.Length)
return null;
9114 var targetId = tabArray[activeTabIndex].Key;
9115 var sessionId = tabArray[activeTabIndex].Value;
9117 if (sessionId ==
null)
9119 var attachResult = SendCommand<dynamic>(DevToolsMethods.TargetAttachToTarget,
new
9123 },
null).GetAwaiter().GetResult();
9125 string newSessionId = attachResult.sessionId;
9128 var newQueue =
new ConcurrentQueue<KeyValuePair<string, string>>();
9129 foreach (var kvp
in tabArray)
9130 newQueue.Enqueue(kvp.Key == targetId
9131 ?
new KeyValuePair<string, string>(kvp.Key, newSessionId)
9134 window.TabQueue = newQueue;
9135 sessionId = newSessionId;
9146 public string GetCurrentTargetId()
9148 var windowArray = _windowSessionsQueue.ToArray();
9149 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
return null;
9150 var tabQueue = windowArray[_activeWindowIndex].TabQueue;
9151 var tabArray = tabQueue.ToArray();
9152 var activeTabIndex = windowArray[_activeWindowIndex].ActiveTabIndex;
9153 if (tabArray.Length == 0 || activeTabIndex >= tabArray.Length)
return null;
9154 return tabArray[activeTabIndex].Key;
9161 public string GetCurrentWindowTargetId()
9163 var windowArray = _windowSessionsQueue.ToArray();
9165 return windowArray[_activeWindowIndex].BrowserContextId;
9177 internal async Task AddTabToQueue(
string targetId,
string sessionId,
bool isActive =
true,
bool doNotGetSendSemaphore =
false)
9179 var windowArray = _windowSessionsQueue.ToArray();
9180 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
9183 var targets = await SendCommand<object>(DevToolsMethods.TargetGetTargets,
new { },
null, 3, doNotGetSendSemaphore).ConfigureAwait(
false);
9184 var target = ((IEnumerable<dynamic>)targets.targetInfos)
9185 .FirstOrDefault(t => t.targetId == targetId && t.type ==
"page");
9189 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Target ID [{targetId}] not found in CDP",
this, GPALObjectType.PuppeteerCommunicator);
9193 string browserContextId = target.browserContextId?.ToString() ??
"";
9195 var newTabQueue =
new ConcurrentQueue<KeyValuePair<string, string>>();
9196 newTabQueue.Enqueue(
new KeyValuePair<string, string>(targetId, sessionId));
9197 _windowSessionsQueue.Enqueue(
new WindowSession(browserContextId, newTabQueue, 0));
9198 _activeWindowIndex = 0;
9199 ((PuppeteerClient)PuppeteerClient)._currentTargetId = targetId;
9200 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Created window with browserContextId [{browserContextId}], added tab ID [{targetId}], Active window [0], tab [0]",
this, GPALObjectType.PuppeteerCommunicator);
9204 var tabQueue = windowArray[_activeWindowIndex].TabQueue;
9205 tabQueue.Enqueue(
new KeyValuePair<string, string>(targetId, sessionId));
9208 var windowList = _windowSessionsQueue.ToList();
9210 windowList[_activeWindowIndex].BrowserContextId,
9212 true == isActive ? tabQueue.Count - 1 : windowList[_activeWindowIndex].ActiveTabIndex
9214 _windowSessionsQueue =
new ConcurrentQueue<WindowSession>();
9215 foreach (var w
in windowList)
9217 _windowSessionsQueue.Enqueue(w);
9219 var newTabIdx = tabQueue.Count - 1;
9221 if (
true == isActive)
9227 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Added tab to active window at index [{newTabIdx}], ID [{targetId}], Tab queue size [{tabQueue.Count}], active tab is now index [{windowList[_activeWindowIndex].ActiveTabIndex}]",
this, GPALObjectType.PuppeteerCommunicator);
9234 internal void SetActiveTabIndex(
int index)
9236 var windowArray = _windowSessionsQueue.ToArray();
9237 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
9239 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No active window for SetActiveTabIndex",
this, GPALObjectType.PuppeteerCommunicator);
9242 var tabQueue = windowArray[_activeWindowIndex].TabQueue;
9243 var tabArray = tabQueue.ToArray();
9244 index = index % tabArray.Length;
9245 if (index < 0) index += tabArray.Length;
9248 var windowList = _windowSessionsQueue.ToList();
9249 windowList[_activeWindowIndex] =
new WindowSession(
9250 windowList[_activeWindowIndex].BrowserContextId,
9251 windowList[_activeWindowIndex].TabQueue,
9254 _windowSessionsQueue =
new ConcurrentQueue<WindowSession>();
9255 foreach (var w
in windowList)
9257 _windowSessionsQueue.Enqueue(w);
9260 ((PuppeteerClient)PuppeteerClient)._currentTargetId = tabArray[index].Key;
9261 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Set active tab index [{index}] in active window, ID [{tabArray[index].Key}]",
this, GPALObjectType.PuppeteerCommunicator);
9270 internal bool RemoveTabFromQueue(
string targetId)
9272 var windowArray = _windowSessionsQueue.ToArray();
9273 if (windowArray.Length == 0 || _activeWindowIndex >= windowArray.Length)
9275 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No active window for RemoveTabFromQueue [{targetId}]",
this, GPALObjectType.PuppeteerCommunicator);
9279 var tabQueue = windowArray[_activeWindowIndex].TabQueue;
9280 var queueList = tabQueue.ToList();
9281 var indexToRemove = queueList.FindIndex(kvp => kvp.Key == targetId);
9282 if (indexToRemove == -1)
9284 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Tab ID [{targetId}] not found in active window queue",
this, GPALObjectType.PuppeteerCommunicator);
9288 queueList.RemoveAt(indexToRemove);
9289 var newTabQueue =
new ConcurrentQueue<KeyValuePair<string, string>>();
9290 foreach (var kvp
in queueList)
9292 newTabQueue.Enqueue(kvp);
9296 var newActiveTabIndex = windowArray[_activeWindowIndex].ActiveTabIndex;
9297 if (newTabQueue.IsEmpty)
9299 newActiveTabIndex = 0;
9301 else if (newActiveTabIndex > indexToRemove)
9303 newActiveTabIndex--;
9305 else if (newActiveTabIndex == indexToRemove)
9307 newActiveTabIndex = Math.Max(0, newActiveTabIndex - 1);
9310 var windowList = _windowSessionsQueue.ToList();
9311 windowList[_activeWindowIndex] =
new WindowSession(
9312 windowList[_activeWindowIndex].BrowserContextId,
9316 _windowSessionsQueue =
new ConcurrentQueue<WindowSession>();
9317 foreach (var w
in windowList)
9319 _windowSessionsQueue.Enqueue(w);
9322 ((PuppeteerClient)PuppeteerClient)._currentTargetId = newTabQueue.IsEmpty ?
null : newTabQueue.ToArray()[newActiveTabIndex].Key;
9323 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Removed tab ID [{targetId}] from active window, Active tab index now [{newActiveTabIndex}], Tab queue size [{newTabQueue.Count}]",
this, GPALObjectType.PuppeteerCommunicator);
9332 public int GetCurrentWindowIndex(
string targetId)
9334 var windows = _windowSessionsQueue.ToArray();
9335 for (
int i = 0; i < windows.Length; i++)
9337 foreach (var tab
in windows[i].TabQueue)
9338 if (tab.Key == targetId)
9349 internal void SetActiveWindowIndex(
int index)
9351 var windowArray = _windowSessionsQueue.ToArray();
9352 if (windowArray.Length == 0)
9354 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No windows for SetActiveWindowIndex",
this, GPALObjectType.PuppeteerCommunicator);
9357 index = index % windowArray.Length;
9358 if (index < 0) index += windowArray.Length;
9359 _activeWindowIndex = index;
9362 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Set active window index [{_activeWindowIndex}], Active tab index [{windowArray[_activeWindowIndex].ActiveTabIndex}], Target ID [{((PuppeteerClient)PuppeteerClient)._currentTargetId}]",
this, GPALObjectType.PuppeteerCommunicator);
9372 internal bool RemoveWindowFromQueue(
string browserContextId =
null,
string targetId =
null)
9374 var windowArray = _windowSessionsQueue.ToArray();
9375 var indexToRemove = -1;
9377 if (browserContextId !=
null)
9379 indexToRemove = Array.FindIndex(windowArray, w => w.BrowserContextId == browserContextId);
9381 else if (targetId !=
null)
9383 for (
int i = 0; i < windowArray.Length; i++)
9385 var tabArray = windowArray[i].TabQueue.ToArray();
9386 if (Array.Exists(tabArray, kvp => kvp.Key == targetId))
9389 browserContextId = windowArray[i].BrowserContextId;
9395 if (indexToRemove == -1)
9397 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Window/Context ID [{browserContextId ?? targetId}] not found",
this, GPALObjectType.PuppeteerCommunicator);
9401 var queueList = _windowSessionsQueue.ToList();
9402 queueList.RemoveAt(indexToRemove);
9404 _windowSessionsQueue =
new ConcurrentQueue<WindowSession>();
9405 foreach (var w
in queueList)
9407 _windowSessionsQueue.Enqueue(w);
9410 if (queueList.Count == 0)
9412 _activeWindowIndex = 0;
9413 ((PuppeteerClient)PuppeteerClient)._currentTargetId =
null;
9414 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"No windows remain after removing window with browserContextId [{browserContextId}]",
this, GPALObjectType.PuppeteerCommunicator);
9418 if (_activeWindowIndex > indexToRemove)
9420 _activeWindowIndex--;
9422 else if (_activeWindowIndex == indexToRemove)
9424 _activeWindowIndex = Math.Max(0, _activeWindowIndex - 1);
9426 ((PuppeteerClient)PuppeteerClient)._currentTargetId = GetCurrentTargetId();
9427 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);
9441 public async Task<bool> ScrollPageAsync(
string direction,
int pagesToScroll,
string s)
9443 double initialScrollY = 0;
9444 bool retVal =
false;
9449 if (direction !=
"up" && direction !=
"down")
9455 var overflowResult = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
9457 expression =
@"getComputedStyle(document.documentElement).overflow",
9458 contextId = CurrentContextId,
9459 returnByValue = true
9460 }, s).ConfigureAwait(
false);
9461 string overflow = overflowResult?[
"result"]?[
"value"].ToString();
9464 if (overflow !=
"hidden")
9466 for (
int count = 0; count < pagesToScroll; count++)
9468 if (
true == (retVal = await scrollPage().ConfigureAwait(
false)))
9474 if (
false == retVal)
9475 for (
int count = 0; count < pagesToScroll; count++)
9478 if (
true == (retVal = await scrollSinglePageApp().ConfigureAwait(
false)))
9484 catch (Exception ex)
9486 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Unable to page [{direction}] for [{pagesToScroll}] pages.",
this, GPALObjectType.PuppeteerClient, ex);
9489 async Task<bool> scrollPage()
9492 var initialScrollResult = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
9494 expression =
@"window.scrollY",
9495 contextId = CurrentContextId,
9496 returnByValue = true
9497 }, s).ConfigureAwait(
false);
9498 initialScrollY = Convert.ToDouble(initialScrollResult?[
"result"]?[
"value"]);
9501 string scrollDirection = direction ==
"down" ?
"window.innerHeight" :
"-window.innerHeight";
9502 await SendCommand<object>(DevToolsMethods.RuntimeEvaluate,
new
9504 expression = $
"window.scrollBy({{ left: 0, top: {scrollDirection}, behavior: 'instant' }})",
9505 contextId = CurrentContextId,
9506 returnByValue = true
9507 }, s).ConfigureAwait(
false);
9510 var afterScrollResult = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
9512 expression =
@"window.scrollY",
9513 contextId = CurrentContextId,
9514 returnByValue = true
9516 }, s).ConfigureAwait(
false);
9517 double afterScrollY = Convert.ToDouble(afterScrollResult?[
"result"]?[
"value"]);
9519 if (afterScrollY != initialScrollY)
9526 async Task<bool> scrollSinglePageApp()
9529 var elementScrollResult = await SendCommand<dynamic>(DevToolsMethods.RuntimeEvaluate,
new
9531 expression =
$@"(function() {{
9532 const isDown = '{direction}' === 'down';
9533 const scrollAmount = isDown ? window.innerHeight : -window.innerHeight;
9534 const scrollable = Array.from(document.querySelectorAll('*')).find(el =>
9535 el.scrollHeight > el.clientHeight && getComputedStyle(el).overflowY !== 'hidden'
9537 const initialScrollTop = scrollable.scrollTop;
9538 scrollable.scrollBy({{ left: 0, top: scrollAmount, behavior: 'instant' }});
9539 return {{ initialScrollTop, scrollTop: scrollable.scrollTop, element: scrollable.className || 'body' }};
9541 contextId = CurrentContextId,
9542 returnByValue = true
9543 }, s).ConfigureAwait(
false);