GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
ElementHelper.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.Collections.ObjectModel;
20using System.Diagnostics;
21using System.Drawing;
22using System.IO;
23using System.Linq;
24using System.Net;
25using System.Net.WebSockets;
26using System.Reflection;
27using System.Runtime.InteropServices;
28using System.Text;
29using System.Text.Json;
30using System.Text.RegularExpressions;
31using System.Threading;
32using System.Threading.Tasks;
33using System.Windows.Controls.Primitives;
34using System.Windows.Forms;
35using System.Windows.Threading;
36//using DocumentFormat.OpenXml.Bibliography;
37//using DocumentFormat.OpenXml.Drawing;
38using Microsoft.Win32;
39using OpenQA.Selenium;
40using OpenQA.Selenium.Interactions;
41using OpenQA.Selenium.Support.UI;
42using Org.BouncyCastle.Security;
44using static GenerallyPositive.Enums;
46using Keys = OpenQA.Selenium.Keys;
47
49{
50
51 internal class ElementHelper
52 {
53 public delegate ReadOnlyCollection<GPALElement> FindWebElementsByDelegate(Browser browser, SelectorPathEntry selectorPath, Selector selector, GPALElement webElement = null, bool isPersistent = false);
54
55 // to avoid repeat messages for the same selector when waiting for a selector
56 static List<string> listOfMessages = new List<string>();
57 static bool supressedMessage = false;
58 static Selector lastSelector = null;
59
60 // how hard ScrollUntilVisible works before it gives up and lets the workflow carry on
61 const int VisibilitySettleChecks = 4; // rechecks after a scroll, to ride out a header sliding in
62 const int VisibilitySettleMs = 100; // wait between those rechecks
63 const int MaxScrollPages = 40; // screens to page through before giving up on finding it
64
65 // grok refactor
81 public static ReadOnlyCollection<GPALElement> FindWebElements(
82 Browser browser,
83 UnitOfWork currentUOW,
84 Selector selector,
85 out bool matchedAll,
86 out List<GPALElement> matchedElements,
87 GPALElement webElement = null,
88 bool callIfHandler = true,
89 bool isPersistent = false)
90 {
91 int retries = 1;
92 bool retry = false;
93 int withAllThatMatch = int.MaxValue;
94
95 // next page button does not go thru .withselector to add browser. i suppose they could all just use this spot.
96 if (null == selector.Browser)
97 selector.Browser = browser;
98
99 dynamic ourRetryElement = webElement;
100 ReadOnlyCollection<GPALElement> retElems = new ReadOnlyCollection<GPALElement>(new List<GPALElement>());
101 string message = string.Empty;
102
103 currentUOW.CurrentSelector = selector;
104
105 if (false == lastSelector?.Name.Equals(selector.Name))
106 {
107 listOfMessages.Clear();
108 supressedMessage = false;
109 lastSelector = selector;
110 }
111 else if (null == lastSelector)
112 lastSelector = selector;
113
114 if (browser.persistentUOW != currentUOW)
115 browser.CurrentUOW.CurrentSelector = selector;
116
117 matchedAll = false;
118 matchedElements = new List<GPALElement>();
119
120 List<FindWebElementsByDelegate> functionList = new List<FindWebElementsByDelegate>
121 {
122 null, FindWebElementsByCss, FindWebElementsByImage, FindWebElementsByText,
123 FindWebElementsByValue, FindWebElementsByXPath, FindWebElementsByName,
124 null, FindWebElementsByClassName, FindWebElementsByPlaceholder, FindWebElementsByHRef
125 };
126
127 foreach (var selectorPathEntry in selector.SelectorPaths)
128 {
129 tryAgain: // NOTE: CAVEAT: goto label - don't like it, but it has to happen sometimes. we cannot have any inner loop as that will break retrying the next selector
130 int textLength = selectorPathEntry.SelectorPath.Length - 1;
131 string abbreviatedText = selectorPathEntry.SelectorPath.Substring(0, Math.Min(7, textLength)) + "..." + selectorPathEntry.SelectorPath.Substring(Math.Max(0, textLength - 7));
132 if (18 > selectorPathEntry.SelectorPath.Length)
133 abbreviatedText = selectorPathEntry.SelectorPath;
134
135 int idx = GetIndexOfEnum<SelectorPathType>((int)selectorPathEntry.SelectorPathType);
136 bool tryNextSelector = false;
137 ReadOnlyCollection<GPALElement> allFoundElems = new ReadOnlyCollection<GPALElement>(new List<GPALElement>());
138 string errorMessage = $"Error finding elements for selector [{selector.Name}][{abbreviatedText}]";
139 int selectorPathIdx = 0;
140
141 try
142 {
143 message = $"Searching for elements for selector [{selector.Name}] using [{selectorPathEntry.SelectorPathType}] [{abbreviatedText}]";
144
145 if (false == listOfMessages.Contains(message))
146 {
147 listOfMessages.Add(message);
148 GPAL.PublishSimpleEvent(GPALEventType.INFO, message, browser, GPALObjectType.Browser);
149 }
150
151 var tmpAllFoundElems = FindElements(browser, currentUOW, selectorPathEntry, selector, webElement, idx, functionList, isPersistent);
152
153 // several finders hand back null rather than an empty collection - an image that did not match,
154 // or an otto xpath with no hits - and that null would replace the empty collection this method
155 // was initialized with, so both the return value and the out param would go null on the most
156 // ordinary outcome there is. nothing found is an empty collection, never null.
157 retElems = allFoundElems = tmpAllFoundElems ?? new ReadOnlyCollection<GPALElement>(new List<GPALElement>());
158 matchedElements = allFoundElems.ToList();
159
160 if (0 < retElems?.Count)
161 {
162 // NOTE: puppeteer and ottomagic give us gpalelements with individual Css on each element, not so for selenium
163 if (true == browser.UseSelenium)
164 foreach (GPALElement gPALElement in allFoundElems)
165 gPALElement.Css = GenerateCssSelector(browser, gPALElement.WebElement);
166
167 var matchingResult = ApplyCustomMatchCriteria(selector, allFoundElems, browser, matchedElements, out tryNextSelector);
168 matchedAll = matchingResult;
169
170 if (tryNextSelector)
171 {
172 retElems = new ReadOnlyCollection<GPALElement>(new List<GPALElement>());
173 matchedElements.Clear();
174 continue;
175 }
176
177 List<GPALElement> simpleMatches = matchedElements;
178 // simple matching applies on top of custom matching, so pass in matchedELements which will be allfound if no custom matching
179 if (selector.MatchCriteria.Count > 0)
180 matchedAll = ApplySimpleMatching(selector, matchedElements, matchedElements, out simpleMatches);
181 else
182 matchedAll = true;
183
184 matchedElements = simpleMatches; // after simple matching, this will contain only matches
185
186 selectorPathEntry.WebSelectorFoundResults = allFoundElems?.ToList();
187 selectorPathEntry.WebSelectorMatchedResults = simpleMatches;
188
189 // if we use .WithAllThatMatch(number) we want up to that number, either in total or per page
190 // int.MaxValue means all rows, so the most specific scope that asked for a limit wins:
191 // unit of work, then browser, then global. if none did, we take them all.
192 if (int.MaxValue != currentUOW.WithAllThatMatch && 0 < currentUOW.WithAllThatMatch)
193 withAllThatMatch = currentUOW.WithAllThatMatch;
194 else if (int.MaxValue != browser.BrowserSettings.WithAllThatMatch && 0 < browser.BrowserSettings.WithAllThatMatch)
195 withAllThatMatch = browser.BrowserSettings.WithAllThatMatch;
196 else if (0 < GPAL.GPALSettings.WithAllThatMatch)
197 withAllThatMatch = GPAL.GPALSettings.WithAllThatMatch; // int.MaxValue if it was never set
198
199 if (int.MaxValue != withAllThatMatch && 0 < simpleMatches?.Count)
200 {
201 List<GPALElement> tmpList = new List<GPALElement>();
202
203 // ensure we copy over only as many results as requested, or how many results we have
204 tmpList.AddRange(simpleMatches.Take(Math.Min(withAllThatMatch, matchedElements?.Count ?? 0)));
205
206 matchedElements = tmpList;
207 }
208
209 foreach (GPALElement element in retElems)
210 {
211 element.Browser = browser;
212 element.Selector = selector;
213 }
214 }
215 }
216 catch (StaleElementReferenceException sereex)
217 {
218 message = errorMessage + $" StaleElementReferenceException: [{sereex.Message}]";
219 retry = ResolveWebElementIssue(browser, currentUOW, ref ourRetryElement, sereex, ref retries, message);
220 }
221 catch (ElementClickInterceptedException ecieex)
222 {
223 message = errorMessage + $" ElementClickInterceptedException: [{ecieex.Message}]";
224 retry = ResolveWebElementIssue(browser, currentUOW, ref ourRetryElement, ecieex, ref retries, message);
225 }
226 catch (ElementNotVisibleException enveex)
227 {
228 message = errorMessage + $" ElementNotVisibleException: [{enveex.Message}]";
229 retry = ResolveWebElementIssue(browser, currentUOW, ref ourRetryElement, enveex, ref retries, message);
230 }
231 catch (GPALException)
232 {
233 throw;
234 }
235 catch (Exception ex)
236 {
237 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, errorMessage, browser, GPALObjectType.Browser, ex);
238 break;
239 }
240
241 if (true == retry && 0 < retries--)
242 {
243 retry = false;
244 goto tryAgain; // NOTE: CAVEAT: goto label
245 }
246
247 selector.ElementsFoundAndMatchedCount = (int)(null != matchedElements ? matchedElements?.Count() : 0);
248
249 message = $"[{retElems?.Count ?? 0}] elements found for selector [{selector.Name}]";
250
251 if (false == listOfMessages.Contains(message))
252 {
253 listOfMessages.Add(message);
254 GPAL.PublishSimpleEvent(GPALEventType.INFO, message, browser, GPALObjectType.Browser);
255
256 if (0 < retElems?.Count && (true == selector.AnyMatchCriteria() || Int32.MaxValue != withAllThatMatch))
257 {
258 var matchCount = selector.MatchCriteria.Count > 0 ? selector.MatchCriteria.Count.ToString() : $"WithAllThatMatch({withAllThatMatch})";
259
260 if ((true == matchCount.Contains("WithAllThatMatch") && 0 < withAllThatMatch) || 0 < selector.MatchCriteria.Count)
261 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{matchedElements?.Count}] elements matched for selector [{selector.Name}] with [{matchCount}] match criteria", browser, GPALObjectType.Browser);
262 }
263 }
264 else if (false == supressedMessage)
265 {
266 GPAL.PublishSimpleEvent(GPALEventType.INFO, "Supressing repeat messages", browser, GPALObjectType.Browser);
267 supressedMessage = true;
268 }
269
270 if (matchedAll || retElems?.Count > 0)
271 break;
272 else if (++selectorPathIdx == selector.SelectorPaths.Count)
273 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"No more selector paths. No elements found.", browser, GPALObjectType.Browser);
274 }
275
276 // false == callIfHandler will be the waitfor call
277 if (true == callIfHandler)
278 CallIfHandlers(browser, currentUOW, retElems, matchedElements, selector, matchedAll);
279
280 return retElems;
281 }
282
283 // Helper to handle shadow DOM or normal search with retries
298 public static ReadOnlyCollection<GPALElement> FindElements(
299 Browser browser,
300 UnitOfWork currentUOW,
301 SelectorSettings.SelectorPathEntry selectorPathEntry,
302 Selector selector,
303 GPALElement gPalElement,
304 int idx,
305 List<FindWebElementsByDelegate> functionList,
306 bool isPersistent)
307 {
308 if (0 < browser.CurrentUOW.ContextPath?.Count && null == gPalElement)
309 {
310
311 if (true == browser.UseOttoMagic)
312 browser.MagicHelper.SwitchToDefaultContent();
313 else if (true == browser.UsePuppeteer)
314 browser.PuppeteerClient.SwitchToDefaultContent().Execute();
315 else if (true == browser.UseSelenium)
316 browser.BrowserDriver.SwitchTo().DefaultContent();
317
318 IEnumerable<UnitOfWork.WebElementWithType> stepsToProcess;
319
320 // this doesn't work as expected
321 //if (true == browser.UseOttoMagic)
322 //{
323 // // OttoMagic special rule:
324 // // Skip ALL IFrames, and only keep ShadowRoot steps that come AFTER the last IFrame in the list.
325 // int lastIframeIndex = browser.CurrentUOW.ContextPath
326 // .Select((step, index) => new { step, index })
327 // .LastOrDefault(x => x.step.elementType == ElementType.IFrame)?.index ?? -1;
328
329 // stepsToProcess = browser.CurrentUOW.ContextPath
330 // .Skip(lastIframeIndex + 1) // everything after the last iframe
331 // .Where(step => step.elementType == ElementType.ShadowRoot || step.elementType == ElementType.Element || step.elementType == ElementType.IFrame); // NOTE: only shadow roots and elements - possibly redundant, but in case we add other types
332
333 //}
334 //else
335 {
336 // puppeteer or selenium have to be iframe aware: process the full original path
337 stepsToProcess = browser.CurrentUOW.ContextPath;
338 }
339
340 // Now walk the (possibly filtered) steps
341 foreach (UnitOfWork.WebElementWithType contextType in stepsToProcess)
342 {
343 if (ElementType.IFrame == contextType.elementType)
344 {
345 if (true == browser.UseOttoMagic)
346 browser.MagicHelper.SwitchToElement(contextType.gPalElement.Css);
347 else if (true == browser.UsePuppeteer)
348 browser.PuppeteerClient.SwitchToFrame(contextType.gPalElement.Css).Execute();
349 else
350 browser.BrowserDriver.SwitchTo().Frame((IWebElement)contextType.gPalElement.WebElement);
351
352 // a search context belongs to one document, so anything scoped in the frame we just left
353 // is stale now - a shadow root from the parent throws "no such shadow root" if we keep
354 // using it here. whatever comes after this step re-establishes the scope.
355 gPalElement = null;
356 }
357 else if (ElementType.ShadowRoot == contextType.elementType)
358 {
359 if (true == browser.UseOttoMagic)
360 browser.MagicHelper.SwitchToShadowRoot(contextType.gPalElement.Css);
361 else if (true == browser.UsePuppeteer)
362 browser.PuppeteerClient.SwitchToShadowRoot(contextType.gPalElement.Css).Execute();
363
364 // scope to THIS step's shadow root, the same one otto and puppeteer just switched into.
365 // selenium has no switch call - handing the search its ShadowRoot context is the switch -
366 // so it has to be this step's root and not the last one added to the unit of work, or a
367 // path with more than one shadow root scopes every step to the innermost one.
368 gPalElement = contextType.gPalElement;
369 }
370 else if (ElementType.Element == contextType.elementType)
371 {
372 gPalElement = contextType.gPalElement; // this is what gets passed down
373 }
374 }
375 }
376
377 int retries = 1;
378 while (retries-- > 0)
379 {
380 try
381 {
382 return functionList[idx](browser, selectorPathEntry, selector, gPalElement, isPersistent);
383 }
384 catch (GPALException)
385 {
386 throw;
387 }
388 catch (Exception ex)
389 {
390 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Error finding elements for selector [{selector.Name}]. Trying next selector path if defined.", browser, GPALObjectType.Browser, ex);
391 break; ; // for retry handler
392 }
393 }
394
395 return new ReadOnlyCollection<GPALElement>(new List<GPALElement>());
396 }
397 // Helper to apply custom matching criteria
411 private static bool ApplyCustomMatchCriteria(
412 Selector selector,
413 ReadOnlyCollection<GPALElement> allFoundElems,
414 Browser browser,
415 List<GPALElement> matchedElements,
416 out bool tryNextSelector,
417 bool AndOrOr = true) // true = AND, false = OR
418 {
419 tryNextSelector = false;
420
421 var universe = allFoundElems?.ToList();
422
423 // initial working set
424 var workingSet = matchedElements.Count > 0
425 ? new List<GPALElement>(matchedElements)
426 : new List<GPALElement>(universe);
427
428 bool matchedAll = true; // default to true, if no custom match is applied to tell us otherwise
429
430 foreach (var matchCriteriaEntry in selector.MatchCriteria)
431 {
432 if (matchCriteriaEntry.MatchType != MatchType.Custom)
433 continue;
434
435 // Determine the source set
436 var source = AndOrOr ? workingSet : universe;
437
438 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
439 $"Applying custom match [{matchCriteriaEntry.WebMatchingFunction.GetInvocationList()[0].Method.Name}] " +
440 $"to {(AndOrOr ? "working set" : "full universe")} ({source.Count} elements)",
441 browser,
442 GPALObjectType.Browser);
443
444 CallIfStatus handled = matchCriteriaEntry.WebMatchingFunction(
445 source.Cast<IGPALElement>().ToList(),
446 out List<IGPALElement> tempMatchedElemsRaw,
447 out matchedAll,
448 selector,
449 matchCriteriaEntry.ExactMatch);
450 List<GPALElement> tempMatchedElems = tempMatchedElemsRaw?.Cast<GPALElement>().ToList();
451
452 // Deduplicate
453 var matchedThisRound = new List<GPALElement>();
454 var roundSet = new HashSet<GPALElement>();
455
456 foreach (var elem in tempMatchedElems ?? new List<GPALElement>())
457 {
458 if (roundSet.Add(elem))
459 {
460 matchedThisRound.Add(elem);
461
462 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
463 $"Element matched custom criteria [{matchCriteriaEntry.WebMatchingFunction.GetInvocationList()[0].Method.Name}] " +
464 $"TagName: [{elem.TagName}], Text: [{elem.Text}]",
465 browser,
466 GPALObjectType.Browser);
467 }
468 }
469
470 if (CallIfStatus.Terminate == handled)
471 {
472 GPAL.PublishSimpleEvent(
473 GPALEventType.EXCEPTION,
474 $"Custom match handler [{matchCriteriaEntry.WebMatchingFunction.GetInvocationList()[0].Method.Name}] requested program termination for selector [{selector.Name}].",
475 browser,
476 GPALObjectType.Browser);
477
478 throw new GPALException(
479 $"Custom match handler [{matchCriteriaEntry.WebMatchingFunction.GetInvocationList()[0].Method.Name}] requested program termination for selector [{selector.Name}].");
480 }
481
482 if (AndOrOr)
483 {
484 // AND = shrink working set
485 workingSet = matchedThisRound;
486 }
487 else
488 {
489 // OR = union into working set
490 var unionSet = new HashSet<GPALElement>(workingSet);
491 foreach (var elem in matchedThisRound)
492 {
493 if (unionSet.Add(elem))
494 {
495 workingSet.Add(elem);
496
497 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
498 $"Element added to OR working set from custom match: TagName: [{elem.TagName}], Text: [{elem.Text}]",
499 browser,
500 GPALObjectType.Browser);
501 }
502 }
503 }
504
505 if (CallIfStatus.Handled == handled)
506 break;
507
508 if (CallIfStatus.TryNext == handled)
509 {
510 tryNextSelector = true;
511 break;
512 }
513 }
514
515 matchedElements.Clear();
516 matchedElements.AddRange(workingSet);
517
518 return matchedAll;
519 }
520
521 // Helper to apply simple matching criteria to elements after custom matching
532 private static bool ApplySimpleMatching(
533 Selector selector,
534 List<GPALElement> matchedElements,
535 List<GPALElement> allFoundElems,
536 out List<GPALElement> matchedElementsOut,
537 bool AndOrOr = true) // true = AND, false = OR
538 {
539 matchedElementsOut = new List<GPALElement>();
540 var universe = allFoundElems?.ToList();
541
542 // initial working set
543 var workingSet = matchedElements.Count > 0
544 ? new List<GPALElement>(matchedElements)
545 : new List<GPALElement>(universe);
546
547 foreach (var matchCriteriaEntry in selector.MatchCriteria)
548 {
549 if (matchCriteriaEntry.MatchType == MatchType.None || matchCriteriaEntry.MatchType == MatchType.Custom)
550 continue;
551
552 // source for this round
553 var source = AndOrOr ? workingSet : universe;
554 var matchedThisRound = new List<GPALElement>();
555 var roundSet = new HashSet<GPALElement>();
556
557 foreach (var element in source)
558 {
559 string elementStringToMatch = null;
560
561 if ("GPALElement" != element.TagName)
562 {
563 switch ((MatchType)((int)matchCriteriaEntry.MatchType & 0xFE))
564 {
565 case MatchType.Text:
566 elementStringToMatch = element.Text;
567 if (string.IsNullOrEmpty(elementStringToMatch))
568 elementStringToMatch = element.GetAttribute("title");
569 if (string.IsNullOrEmpty(elementStringToMatch))
570 elementStringToMatch = element.GetAttribute("value");
571 break;
572 case MatchType.Href:
573 elementStringToMatch = element.GetAttribute("href");
574 break;
575 case MatchType.Src:
576 elementStringToMatch = element.GetAttribute("src");
577 break;
578 case MatchType.Placeholder:
579 elementStringToMatch = element.GetAttribute("placeholder");
580 break;
581 case MatchType.Value:
582 elementStringToMatch = element.GetAttribute("value");
583 break;
584 case MatchType.Attribute:
585 elementStringToMatch = element.GetAttribute(selector.AttributeName);
586 break;
587 }
588
589 if (!string.IsNullOrEmpty(elementStringToMatch))
590 {
591 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
592 $"String to match [{matchCriteriaEntry.StringToMatch}] element text [{elementStringToMatch}]");
593
594 bool isMatch = false;
595
596 if ((int)(matchCriteriaEntry.MatchType & MatchType.Regex) == 1)
597 {
598 var mc = Regex.Matches(elementStringToMatch, matchCriteriaEntry.StringToMatch);
599 if (mc.Count > 0)
600 isMatch = true;
601 }
602 else
603 {
604 if (false == matchCriteriaEntry.ExactMatch && elementStringToMatch.Contains(matchCriteriaEntry.StringToMatch))
605 isMatch = true;
606 else if (true == matchCriteriaEntry.ExactMatch && elementStringToMatch.Equals(matchCriteriaEntry.StringToMatch))
607 isMatch = true;
608 }
609
610 if (isMatch && roundSet.Add(element))
611 {
612 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Adding [{element.TagName}][{element.Text}]");
613 matchedThisRound.Add(element);
614 }
615 }
616 }
617 else
618 {
619 if (roundSet.Add(element))
620 matchedThisRound.Add(element);
621 }
622 }
623
624 if (AndOrOr)
625 {
626 // AND = narrow
627 workingSet = matchedThisRound;
628 }
629 else
630 {
631 // OR = union
632 var unionSet = new HashSet<GPALElement>(workingSet);
633 foreach (var elem in matchedThisRound)
634 {
635 if (unionSet.Add(elem))
636 {
637 workingSet.Add(elem);
638 }
639 }
640 }
641 }
642
643 matchedElementsOut = workingSet;
644 return universe.Count == matchedElementsOut.Count;
645 }
646
647 // TODO: what do these values mean outside of the bubble handlers?
648 // currentUOW.callFoundHandled
649 // currentUOW.callNotFoundHandled
662 private static void CallIfHandlers(Browser browser, UnitOfWork currentUOW, ReadOnlyCollection<GPALElement> foundElements, List<GPALElement> matchedElements, Selector selector, bool matchedAll)
663 {
664 string str = null;
665 string currentMethodName = null;
666 int selectorsFoundOrMatched = selector.AnyMatchCriteria() ? matchedElements?.Count ?? 0 : foundElements?.Count ?? 0;
667 UnitOfWork safeUOW;
668
669 // TODO: do we care about matchedall?
670
671 // bubble thru handlers based upon handler return value
672 // 0 = not handled, bubble to next handler
673 // 1 = handled and continue with the program
674 // -1 = unexpected error and terminate program
675 if (0 < selectorsFoundOrMatched)
676 {
677 if (0 != (selector.SelectorSettings.CallIfFound?.Count ?? 0))
678 {
679 foreach (Browser.CallIfDelegate func in selector.SelectorSettings.CallIfFound)
680 {
681 if (true == selector.RemovedMethods.Contains(func))
682 continue; // soft deleted by RemoveCallIfHandlerEverywhere
683
684 bool safeActionCalled = browser.CurrentUOW.ActionCalled;
685 safeUOW = browser.CurrentUOW;
686 browser.CurrentUOW.ActionCalled = true; // this will allow the callif method to immediately use browser.WaitFor
687
688 currentMethodName = func.Method.Name;
689 str = $"Browser Selector CallIfFound handler [{currentMethodName}] requested program termination on selector [{selector.Name}] [{selector.SelectorPaths[0].SelectorPath}]";
690
691 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Invoking Selector CallIfFound [{func.Method.Name}]", browser, GPALObjectType.Browser);
692
693 selector.CallIfFoundHandled = func(browser, foundElements?.Cast<IGPALElement>().ToList(), matchedElements?.Cast<IGPALElement>().ToList(), selector, matchedAll);
694
695 browser.CurrentUOW = safeUOW;
696 browser.CurrentUOW.ActionCalled = safeActionCalled;
697
698 if (CallIfStatus.NotHandled == selector.CallIfFoundHandled)
699 continue;
700 else
701 break;
702 }
703
704 if (CallIfStatus.Terminate == selector.CallIfFoundHandled)
705 {
706 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, browser, GPALObjectType.Browser);
707 throw new GPALException(str);
708 }
709 }
710
711 // selector.CallIfFoundHandled - if handled by selector handler, don't call UOW handler
712 if (CallIfStatus.NotHandled == selector.CallIfFoundHandled && 0 < (currentUOW.CallIfFound?.Count ?? 0))
713 {
714 foreach (Browser.CallIfDelegate func in currentUOW.CallIfFound)
715 {
716 if (true == selector.RemovedMethods.Contains(func))
717 continue; // soft deleted by RemoveCallIfHandlerEverywhere
718
719 safeUOW = browser.CurrentUOW;
720
721 currentMethodName = func.Method.Name;
722 str = $"Browser UOW Selector CallIfFound handler [{currentMethodName}] requested program termination on selector [{selector.Name}] [{selector.SelectorPaths[0].SelectorPath}]";
723
724 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Invoking UOW CallIfFound [{func.Method.Name}]", browser, GPALObjectType.Browser);
725
726 currentUOW.CallIfFoundHandled = func(browser, foundElements?.Cast<IGPALElement>().ToList(), matchedElements?.Cast<IGPALElement>().ToList(), selector, matchedAll);
727
728 browser.CurrentUOW = safeUOW;
729
730 if (CallIfStatus.NotHandled == currentUOW.CallIfFoundHandled)
731 continue;
732 else
733 break;
734 }
735
736 if (CallIfStatus.Terminate == currentUOW.CallIfFoundHandled)
737 {
738 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, browser, GPALObjectType.Browser);
739 throw new GPALException(str);
740 }
741 }
742
743 // currentUOW.CallIfFoundHandled - if handled by UOW handler, don't call global handler
744 if (CallIfStatus.NotHandled == selector.CallIfFoundHandled && 0 == currentUOW.CallIfFoundHandled && 0 != (GPAL.GPALSettings.CallIfFoundList?.Count ?? 0))
745 {
746 foreach (Browser.CallIfDelegate func in GPAL.GPALSettings.CallIfFoundList)
747 {
748 if (true == selector.RemovedMethods.Contains(func))
749 continue; // soft deleted by RemoveCallIfHandlerEverywhere
750
751 safeUOW = browser.CurrentUOW;
752
753 currentMethodName = func.Method.Name;
754 str = $"Gloabl CallIfFound handler [{currentMethodName}] requested program termination on selector [{selector.Name}] [{selector.SelectorPaths[0].SelectorPath}]";
755
756 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Invoking Global CallIfFound [{func.Method.Name}]", browser, GPALObjectType.Browser);
757
758 GPAL.GPALSettings.CallIfFoundHandled = func(browser, foundElements?.Cast<IGPALElement>().ToList(), matchedElements?.Cast<IGPALElement>().ToList(), selector, matchedAll);
759
760 browser.CurrentUOW = safeUOW;
761
762 if (CallIfStatus.NotHandled == GPAL.GPALSettings.CallIfFoundHandled)
763 continue;
764 else
765 break;
766 }
767
768 if (CallIfStatus.Terminate == (GPAL.GPALSettings.CallIfFoundHandled))
769 {
770 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, browser, GPALObjectType.Browser);
771 throw new GPALException(str);
772 }
773 }
774 }
775 else
776 {
777 if (0 < selector.SelectorSettings.CallIfNotFound.Count)
778 {
779 foreach (Browser.CallIfDelegate func in selector.SelectorSettings.CallIfNotFound)
780 {
781 if (true == selector.RemovedMethods.Contains(func))
782 continue; // soft deleted by RemoveCallIfHandlerEverywhere
783
784 safeUOW = browser.CurrentUOW;
785
786 currentMethodName = func.Method.Name;
787 str = $"Browser Selector CallIfNotFound handler [{currentMethodName}] requested program termination on selector [{selector.Name}] [{selector.SelectorPaths[0].SelectorPath}[";
788
789 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Invoking Selector CallIfNotFound [{func.Method.Name}]", browser, GPALObjectType.Browser);
790
791 selector.CallIfNotFoundHandled = func(browser, foundElements?.Cast<IGPALElement>().ToList(), matchedElements?.Cast<IGPALElement>().ToList(), selector, matchedAll);
792
793 browser.CurrentUOW = safeUOW;
794
795 if (CallIfStatus.NotHandled == selector.CallIfNotFoundHandled)
796 continue;
797 else
798 break;
799 }
800
801 if (CallIfStatus.Terminate == selector.CallIfNotFoundHandled)
802 {
803 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, browser, GPALObjectType.Browser);
804 throw new GPALException(str);
805 }
806 }
807
808 if (0 == selector.CallIfNotFoundHandled && 0 < currentUOW.CallIfNotFound.Count)
809 {
810 foreach (Browser.CallIfDelegate func in currentUOW.CallIfNotFound)
811 {
812 if (true == selector.RemovedMethods.Contains(func))
813 continue; // soft deleted by RemoveCallIfHandlerEverywhere
814
815 safeUOW = browser.CurrentUOW;
816
817 currentMethodName = func.Method.Name;
818 str = $"Browser UOW Selector CallIfNotFound handler [{currentMethodName}] requested program termination on selector [{selector.Name}] [{selector.SelectorPaths[0].SelectorPath}]";
819
820 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Invoking UOW CallIfNotFound [{func.Method.Name}]", browser, GPALObjectType.Browser);
821
822 currentUOW.CallIfNotFoundHandled = func(browser, foundElements?.Cast<IGPALElement>().ToList(), matchedElements?.Cast<IGPALElement>().ToList(), selector, matchedAll);
823
824 browser.CurrentUOW = safeUOW;
825
826 if (CallIfStatus.NotHandled == currentUOW.CallIfNotFoundHandled)
827 continue;
828 else
829 break;
830 }
831
832 if (CallIfStatus.Terminate == currentUOW.CallIfNotFoundHandled)
833 {
834 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, browser, GPALObjectType.Browser);
835 throw new GPALException(str);
836 }
837 }
838 // currentUOW.CallIfNotFoundHandled - if handled by UOW handler, don't call global handler
839 if (0 == selector.CallIfNotFoundHandled && 0 == currentUOW.CallIfNotFoundHandled && 0 < GPAL.GPALSettings.CallIfNotFoundList.Count)
840 {
841 foreach (Browser.CallIfDelegate func in GPAL.GPALSettings.CallIfNotFoundList)
842 {
843 if (true == selector.RemovedMethods.Contains(func))
844 continue; // soft deleted by RemoveCallIfHandlerEverywhere
845
846 safeUOW = browser.CurrentUOW;
847
848 currentMethodName = func.Method.Name;
849 str = $"Gloabl CallIfNotFound handler [{currentMethodName}] requested program termination on selector [{selector.Name}] [{selector.SelectorPaths[0].SelectorPath}]";
850
851 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Invoking Global CallIfNotFound [{func.Method.Name}]", browser, GPALObjectType.Browser);
852
853 GPAL.GPALSettings.CallIfNotFoundHandled = func(browser, foundElements?.Cast<IGPALElement>().ToList(), matchedElements?.Cast<IGPALElement>().ToList(), selector, matchedAll);
854
855 browser.CurrentUOW = safeUOW;
856
857 if (CallIfStatus.NotHandled == GPAL.GPALSettings.CallIfNotFoundHandled)
858 continue;
859 else
860 break;
861 }
862
863 if (CallIfStatus.Terminate == GPAL.GPALSettings.CallIfNotFoundHandled)
864 {
865 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, browser, GPALObjectType.Browser);
866 throw new GPALException(str);
867 }
868 }
869
870 if (true == selector.StopOnNotFound || true == GPAL.StopOnNotFound)
871 {
872 str = $@"{(true == selector.StopOnNotFound ? "selector." : "GPAL.")}StopOnNotFound set, terminating on Not Found element for [{selector.Name}][{selector.SelectorPaths[0].SelectorPath}]";
873 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, browser, GPALObjectType.Browser);
874 throw new GPALException(str);
875 }
876 }
877 }
888 private static ReadOnlyCollection<GPALElement> FindWebElementsByCss(Browser browser, SelectorPathEntry selectorPath, Selector selector, GPALElement webElement = null, bool isPersistent = false)
889 {
890 bool searchForElement = selector.SearchForSelector;
891 ReadOnlyCollection<IWebElement> elems;
892 ReadOnlyCollection<GPALElement> GPALelems = null;
893
894 if (browser.UseOttoMagic)
895 {
896 List<GPALElement> result = null;
897
898 if (false == webElement?.IsShadowRoot)
899 browser.MagicHelper.SwitchToElement(webElement.Css);
900
901 if (true == isPersistent)
902 result = browser.MagicHelper
903 .QueryPersistentSelectors(selectorPath.SelectorPath);
904 else
905 result = browser.MagicHelper
906 .QuerySelectors(selectorPath.SelectorPath);
907
908 if (null != result)
909 GPALelems = new ReadOnlyCollection<GPALElement>(result);
910 }
911 else if (true == browser.UsePuppeteer)
912 {
913 if (false == webElement?.IsShadowRoot)
914 browser.PuppeteerClient.InElement(webElement.Css).Execute();
915
916 var result2 = Task.Run(() => browser.PuppeteerCommunicator.EvaluateSelector(selectorPath.SelectorPath));
917 if (null != result2)
918 GPALelems = new ReadOnlyCollection<GPALElement>(result2.Result);
919 }
920 else
921 {
922 string css = selectorPath.SelectorPath;
923
924 elems = webElement?.WebElement != null
925 ? webElement.WebElement.FindElements(By.CssSelector(css))
926 // webelement is the shadowroot
927 //: browser.CurrentUOW.ShadowRoot?.WebElement != null
928 // ? browser.CurrentUOW.ShadowRoot.WebElement.FindElements(By.CssSelector(css))
929 : browser.BrowserDriver.FindElements(By.CssSelector(css));
930
931 // the search above ran against the closed root's host, which is all webdriver will ever give us.
932 // finding nothing means the target is inside the closed root, so hand the host back - the same
933 // contract otto uses - and a workflow can still click relative to its corner. persistent
934 // selectors are excluded: they are polled speculatively, and a host that always matches would
935 // fire their CallIf handlers on every sweep.
936 if (0 == elems.Count && true == webElement?.IsClosedShadowRoot && false == isPersistent)
937 return new ReadOnlyCollection<GPALElement>(new List<GPALElement>() { webElement });
938
939 GPALelems = ToGPALElements(browser, selector, elems);
940 }
941
942 return GPALelems;
943 }
955 private static ReadOnlyCollection<GPALElement> FindWebElementsByImage(Browser browser, SelectorPathEntry selectorPath, Selector selector, GPALElement webElement = null, bool isPersistent = false)
956 {
957 bool searchForElement = selector.SearchForSelector;
958
959 ReadOnlyCollection<GPALElement> elems = null;
960
961 Rectangle foundImageBounds = new Rectangle();
962 List<GPALElement> retWebElement = new List<GPALElement>(); ;
963
964 if (null == selectorPath.Image)
965 {
966 try
967 {
968 selectorPath.Image = Image.FromFile(selectorPath.SelectorPath);
969 }
970 catch (GPALException)
971 {
972 throw;
973 }
974 catch (Exception ex)
975 {
976 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to load image for [{selector.Name}].", browser, GPALObjectType.Browser, ex);
977 return null;
978 }
979 }
980
981 foundImageBounds = ((ImageHelper)GPAL.ImageHelper).FindImage(selectorPath.SelectorPath, browser); // first check, is image we seek already on-screen?
982
983 // page down the browser page looking for the image (just send page down key)
984 if (true == searchForElement)
985 {
986 if (true == browser.UseSelenium)
987 {
988 var bodyElement = browser.BrowserDriver.FindElement(By.TagName("body"));
989
990 bodyElement.Click(); // bring focus from the address bar to the document body
991 }
992
993 // Not found, start from top of page
994 if (true == foundImageBounds.IsEmpty)
995 _ = browser.PageTop;
996
997 while (true == foundImageBounds.IsEmpty && false == IsEndOfPage(browser, webElement))
998 {
999 _ = browser.PageDown;
1000 Thread.Sleep(250);
1001 foundImageBounds = ((ImageHelper)GPAL.ImageHelper).FindImage(selectorPath.SelectorPath, browser);
1002 }
1003 }
1004
1005 if (false == foundImageBounds.IsEmpty)
1006 {
1007 // create a psuedo webelement to use in the subsequent methods
1008 // headless matched against a visible-tab capture so the coordinates are viewport relative,
1009 // windowed matched against the screen so they are already absolute - record which, the
1010 // downstream mouse paths need to know whose origin these are measured from
1011 retWebElement.Add(new GPALElement(foundImageBounds.Location, foundImageBounds.Size, selectorPath.SelectorPathType.ToString(), browser.BrowserDriver, "GPALElement")
1012 {
1013 Browser = browser,
1014 Selector = selector,
1015 CoordinateSpace = true == browser.BrowserSettings.UseHeadless ? CoordinateSpace.Viewport : CoordinateSpace.Screen
1016 });
1017 elems = new ReadOnlyCollection<GPALElement>(retWebElement);
1018 // TODO: highlight the found element briefly with a border? .WithHighlightFoundImage
1019 }
1020
1021 return elems;
1022 }
1033 private static ReadOnlyCollection<GPALElement> FindWebElementsByXPath(Browser browser, SelectorPathEntry selectorPath, Selector selector, GPALElement webElement = null, bool isPersistent = false)
1034 {
1035 bool searchForElement = selector.SearchForSelector;
1036
1037 ReadOnlyCollection<IWebElement> elems;
1038 ReadOnlyCollection<GPALElement> GPALelems = null;
1039
1040 if (browser.UseOttoMagic)
1041 {
1042 List<GPALElement> result = null;
1043
1044 if (false == webElement?.IsShadowRoot)
1045 browser.MagicHelper.SwitchToElement(webElement.Css);
1046
1047 if (true == isPersistent)
1048 result = browser.MagicHelper
1049 .EvaluateAllPersistent(selectorPath.SelectorPath);
1050 else
1051 result = browser.MagicHelper
1052 .EvaluateAll(selectorPath.SelectorPath);
1053
1054 if (null != result)
1055 GPALelems = new ReadOnlyCollection<GPALElement>(result);
1056 }
1057 else if (true == browser.UsePuppeteer)
1058 {
1059 if (false == webElement?.IsShadowRoot)
1060 browser.PuppeteerClient.InElement(webElement.Css).Execute();
1061
1062 var result2 = Task.Run(() => browser.PuppeteerCommunicator.EvaluateSelector(selectorPath.SelectorPath));
1063
1064 if (null != result2)
1065 GPALelems = new ReadOnlyCollection<GPALElement>(result2.Result);
1066 }
1067 else
1068 {
1069 string xpath = selectorPath.SelectorPath;
1070
1071 elems = webElement?.WebElement != null
1072 ? webElement.WebElement.FindElements(By.XPath(xpath))
1073 : browser.BrowserDriver.FindElements(By.XPath(xpath));
1074
1075 // closed shadow root - see the CSS search, the host is all we can offer, and never for a
1076 // persistent selector
1077 if (0 == elems.Count && true == webElement?.IsClosedShadowRoot && false == isPersistent)
1078 return new ReadOnlyCollection<GPALElement>(new List<GPALElement>() { webElement });
1079
1080 GPALelems = ToGPALElements(browser, selector, elems);
1081 }
1082
1083 return GPALelems;
1084 }
1101 internal static ReadOnlyCollection<GPALElement> ToGPALElements(Browser browser, Selector selector, ReadOnlyCollection<IWebElement> elems)
1102 {
1103 List<GPALElement> gPALElements = new List<GPALElement>();
1104
1105 if (0 < elems.Count)
1106 {
1107 string script = @"
1108 return arguments[0].map(function (element) {
1109 var rect = element.getBoundingClientRect();
1110 var style = window.getComputedStyle(element);
1111
1112 return {
1113 tag: element.tagName ? element.tagName.toLowerCase() : null,
1114 text: element.innerText,
1115 enabled: false === element.disabled || undefined === element.disabled,
1116 selected: true === element.checked || true === element.selected,
1117 displayed: (0 < element.getClientRects().length) && 'hidden' !== style.visibility && 'none' !== style.display,
1118 x: Math.round(rect.left + window.pageXOffset),
1119 y: Math.round(rect.top + window.pageYOffset),
1120 w: Math.round(rect.width),
1121 h: Math.round(rect.height),
1122 href: element.getAttribute('href'),
1123 src: element.getAttribute('src'),
1124 value: undefined !== element.value ? element.value : element.getAttribute('value'),
1125 placeholder: element.getAttribute('placeholder'),
1126 type: element.getAttribute('type')
1127 };
1128 });
1129 ";
1130
1131 string pageUrl = browser.BrowserDriver.Url;
1132 object answered = ((IJavaScriptExecutor)browser.BrowserDriver).ExecuteScript(script, elems.ToList());
1133 List<object> properties = (answered as IEnumerable<object>)?.ToList();
1134
1135 for (int idx = 0; idx < elems.Count; idx++)
1136 {
1137 // a page that could not answer for its own elements leaves the old path as the way out, one
1138 // element at a time, rather than handing back elements with nothing in them
1139 GPALElement gPALElement = properties?.Count == elems.Count && properties[idx] is Dictionary<string, object> bag
1140 ? new GPALElement(elems[idx], selector.AttributeName, bag, pageUrl)
1141 : new GPALElement(elems[idx], selector.AttributeName);
1142
1143 gPALElement.Browser = browser;
1144 gPALElement.Selector = selector;
1145
1146 gPALElements.Add(gPALElement);
1147 }
1148 }
1149
1150 return new ReadOnlyCollection<GPALElement>(gPALElements);
1151 }
1163 private static ReadOnlyCollection<GPALElement> FindWebElementsByText(Browser browser, SelectorPathEntry selectorPath, Selector selector, GPALElement webElement = null, bool isPersistent = false)
1164 {
1165 bool searchForElement = selector.SearchForSelector;
1166 ReadOnlyCollection<IWebElement> elems;
1167 ReadOnlyCollection<GPALElement> GPALelems = null;
1168 string xpathSelector = $"//*[contains(text(), '{selectorPath.SelectorPath}')]"; // Same XPath as Selenium
1169
1170 if (browser.UseOttoMagic)
1171 {
1172 List<GPALElement> result = null;
1173
1174 if (false == webElement?.IsShadowRoot)
1175 browser.MagicHelper.SwitchToElement(webElement.Css);
1176
1177 if (true == isPersistent)
1178 result = browser.MagicHelper
1179 .EvaluateAllPersistent(xpathSelector);
1180 else
1181 result = browser.MagicHelper
1182 .EvaluateAll(xpathSelector);
1183
1184 if (null != result)
1185 GPALelems = new ReadOnlyCollection<GPALElement>(result);
1186 }
1187 else if (true == browser.UsePuppeteer)
1188 {
1189 if (false == webElement?.IsShadowRoot)
1190 browser.PuppeteerClient.InElement(webElement.Css).Execute();
1191
1192 var result2 = Task.Run(() => browser.PuppeteerCommunicator.EvaluateSelector(xpathSelector));
1193
1194 if (null != result2)
1195 GPALelems = new ReadOnlyCollection<GPALElement>(result2.Result);
1196 }
1197 else
1198 {
1199 if (webElement != null)
1200 xpathSelector = $".{xpathSelector}"; // Make relative XPath for webElement scope
1201
1202 elems = webElement?.WebElement != null
1203 ? webElement.WebElement.FindElements(By.XPath(xpathSelector))
1204 : browser.BrowserDriver.FindElements(By.XPath(xpathSelector));
1205
1206 GPALelems = ToGPALElements(browser, selector, elems);
1207 }
1208
1209
1210 return GPALelems;
1211 }
1222 private static ReadOnlyCollection<GPALElement> FindWebElementsByHRef(Browser browser, SelectorPathEntry selectorPath, Selector selector, GPALElement webElement = null, bool isPersistent = false)
1223 {
1224 bool searchForElement = selector.SearchForSelector;
1225 ReadOnlyCollection<IWebElement> elems;
1226 ReadOnlyCollection<GPALElement> GPALelems = null;
1227 string cssSelector = $"a[href*='{selectorPath.SelectorPath}']"; // Matches <a> elements with href containing the substring
1228
1229 if (browser.UseOttoMagic)
1230 {
1231 List<GPALElement> result = null;
1232
1233 if (false == webElement?.IsShadowRoot)
1234 browser.MagicHelper.SwitchToElement(webElement.Css);
1235
1236 if (true == isPersistent)
1237 result = browser.MagicHelper
1238 .QueryPersistentSelectors(cssSelector);
1239 else
1240 result = browser.MagicHelper
1241 .QuerySelectors(cssSelector);
1242
1243 if (null != result)
1244 GPALelems = new ReadOnlyCollection<GPALElement>(result);
1245 }
1246 else if (true == browser.UsePuppeteer)
1247 {
1248 if (false == webElement?.IsShadowRoot)
1249 browser.PuppeteerClient.InElement(webElement.Css).Execute();
1250
1251 var result2 = Task.Run(() => browser.PuppeteerCommunicator.EvaluateSelector(cssSelector));
1252
1253 if (null != result2)
1254 GPALelems = new ReadOnlyCollection<GPALElement>(result2.Result);
1255 }
1256 else
1257 {
1258 elems = webElement?.WebElement != null
1259 ? webElement.WebElement.FindElements(By.PartialLinkText(selectorPath.SelectorPath))
1260 : browser.BrowserDriver.FindElements(By.PartialLinkText(selectorPath.SelectorPath));
1261
1262 GPALelems = new ReadOnlyCollection<GPALElement>(
1263 elems.Select(e => new GPALElement(e, selector.AttributeName) { Browser = browser, Selector = selector }).ToList()
1264 );
1265 }
1266
1267 return GPALelems;
1268 }
1279 private static ReadOnlyCollection<GPALElement> FindWebElementsByClassName(Browser browser, SelectorPathEntry selectorPath, Selector selector, GPALElement webElement = null, bool isPersistent = false)
1280 {
1281 bool searchForElement = selector.SearchForSelector;
1282 ReadOnlyCollection<IWebElement> elems;
1283 ReadOnlyCollection<GPALElement> GPALelems = null;
1284 string cssSelector = $".{selectorPath.SelectorPath}";
1285
1286 if (browser.UseOttoMagic)
1287 {
1288 List<GPALElement> result = null;
1289
1290 if (false == webElement?.IsShadowRoot)
1291 browser.MagicHelper.SwitchToElement(webElement.Css);
1292
1293 if (true == isPersistent)
1294 result = browser.MagicHelper
1295 .QueryPersistentSelectors(cssSelector);
1296 else
1297 result = browser.MagicHelper
1298 .QuerySelectors(cssSelector);
1299
1300 if (null != result)
1301 GPALelems = new ReadOnlyCollection<GPALElement>(result);
1302 }
1303 else if (true == browser.UsePuppeteer)
1304 {
1305 if (false == webElement?.IsShadowRoot)
1306 browser.PuppeteerClient.InElement(webElement.Css).Execute();
1307
1308 var result2 = Task.Run(() => browser.PuppeteerCommunicator.EvaluateSelector(cssSelector));
1309 if (null != result2)
1310 GPALelems = new ReadOnlyCollection<GPALElement>(result2.Result);
1311 }
1312 else
1313 {
1314 elems = webElement?.WebElement != null
1315 ? webElement.WebElement.FindElements(By.ClassName(selectorPath.SelectorPath))
1316 : browser.BrowserDriver.FindElements(By.ClassName(selectorPath.SelectorPath));
1317
1318 GPALelems = new ReadOnlyCollection<GPALElement>(
1319 elems.Select(e => new GPALElement(e, selector.AttributeName) { Browser = browser, Selector = selector }).ToList()
1320 );
1321 }
1322
1323 return GPALelems;
1324 }
1335 private static ReadOnlyCollection<GPALElement> FindWebElementsByName(Browser browser, SelectorPathEntry selectorPath, Selector selector, GPALElement webElement = null, bool isPersistent = false)
1336 {
1337 bool searchForElement = selector.SearchForSelector;
1338 ReadOnlyCollection<IWebElement> elems;
1339 ReadOnlyCollection<GPALElement> GPALelems = null;
1340 string cssSelector = $"[name='{selectorPath.SelectorPath}']";
1341
1342 if (browser.UseOttoMagic)
1343 {
1344 List<GPALElement> result = null;
1345
1346 if (false == webElement?.IsShadowRoot)
1347 browser.MagicHelper.SwitchToElement(webElement.Css);
1348
1349 if (true == isPersistent)
1350 result = browser.MagicHelper
1351 .QueryPersistentSelectors(cssSelector);
1352 else
1353 result = browser.MagicHelper
1354 .QuerySelectors(cssSelector);
1355
1356 if (null != result)
1357 GPALelems = new ReadOnlyCollection<GPALElement>(result);
1358 }
1359 else if (true == browser.UsePuppeteer)
1360 {
1361 if (false == webElement?.IsShadowRoot)
1362 browser.PuppeteerClient.InElement(webElement.Css).Execute();
1363
1364 var result2 = Task.Run(() => browser.PuppeteerCommunicator.EvaluateSelector(cssSelector));
1365
1366 if (null != result2)
1367 GPALelems = new ReadOnlyCollection<GPALElement>(result2.Result);
1368 }
1369 else
1370 {
1371 elems = webElement?.WebElement != null
1372 ? webElement.WebElement.FindElements(By.Name(selectorPath.SelectorPath))
1373 : browser.BrowserDriver.FindElements(By.Name(selectorPath.SelectorPath));
1374
1375 GPALelems = new ReadOnlyCollection<GPALElement>(
1376 elems.Select(e => new GPALElement(e, selector.AttributeName) { Browser = browser, Selector = selector }).ToList()
1377 );
1378 }
1379
1380 return GPALelems;
1381 }
1393 private static ReadOnlyCollection<GPALElement> FindWebElementsByValue(Browser browser, SelectorPathEntry selectorPath, Selector selector, GPALElement webElement = null, bool isPersistent = false)
1394 {
1395 bool searchForElement = selector.SearchForSelector;
1396 ReadOnlyCollection<IWebElement> elems;
1397 ReadOnlyCollection<GPALElement> GPALelems = null;
1398 string cssSelector = $"[value='{selectorPath.SelectorPath}']";
1399
1400 if (browser.UseOttoMagic)
1401 {
1402 List<GPALElement> result = null;
1403
1404 if (false == webElement?.IsShadowRoot)
1405 browser.MagicHelper.SwitchToElement(webElement.Css);
1406
1407 if (true == isPersistent)
1408 result = browser.MagicHelper
1409 .QueryPersistentSelectors(cssSelector);
1410 else
1411 result = browser.MagicHelper
1412 .QuerySelectors(cssSelector);
1413
1414 if (null != result)
1415 GPALelems = new ReadOnlyCollection<GPALElement>(result);
1416 }
1417 else if (true == browser.UsePuppeteer)
1418 {
1419 if (false == webElement?.IsShadowRoot)
1420 browser.PuppeteerClient.InElement(webElement.Css).Execute();
1421
1422 var result2 = Task.Run(() => browser.PuppeteerCommunicator.EvaluateSelector(cssSelector));
1423 if (null != result2)
1424 GPALelems = new ReadOnlyCollection<GPALElement>(result2.Result);
1425 }
1426 else
1427 {
1428 elems = webElement?.WebElement != null
1429 ? webElement.WebElement.FindElements(By.CssSelector($"[value='{selectorPath.SelectorPath}']"))
1430 : browser.BrowserDriver.FindElements(By.CssSelector($"[value='{selectorPath.SelectorPath}']"));
1431
1432 GPALelems = new ReadOnlyCollection<GPALElement>(
1433 elems.Select(e => new GPALElement(e, selector.AttributeName) { Browser = browser, Selector = selector }).ToList()
1434 );
1435 }
1436
1437 return GPALelems;
1438 }
1449 private static ReadOnlyCollection<GPALElement> FindWebElementsByPlaceholder(Browser browser, SelectorPathEntry selectorPath, Selector selector, GPALElement webElement = null, bool isPersistent = false)
1450 {
1451 bool searchForElement = selector.SearchForSelector;
1452 ReadOnlyCollection<IWebElement> elems;
1453 ReadOnlyCollection<GPALElement> GPALelems = null;
1454 string cssSelector = $"[placeholder='{selectorPath.SelectorPath}']"; // Any element with placeholder
1455
1456 if (browser.UseOttoMagic)
1457 {
1458 List<GPALElement> result = null;
1459
1460 if (false == webElement?.IsShadowRoot)
1461 browser.MagicHelper.SwitchToElement(webElement.Css);
1462
1463 if (true == isPersistent)
1464 result = browser.MagicHelper
1465 .QueryPersistentSelectors(cssSelector);
1466 else
1467 result = browser.MagicHelper
1468 .QuerySelectors(cssSelector);
1469
1470 if (null != result)
1471 GPALelems = new ReadOnlyCollection<GPALElement>(result);
1472 }
1473 else if (true == browser.UsePuppeteer)
1474 {
1475 if (false == webElement?.IsShadowRoot)
1476 browser.PuppeteerClient.InElement(webElement.Css).Execute();
1477
1478 var result2 = Task.Run(() => browser.PuppeteerCommunicator.EvaluateSelector(cssSelector));
1479 if (null != result2)
1480 GPALelems = new ReadOnlyCollection<GPALElement>(result2.Result);
1481 }
1482 else
1483 {
1484 elems = webElement?.WebElement != null
1485 ? webElement.WebElement.FindElements(By.CssSelector($"[placeholder='{selectorPath.SelectorPath}']"))
1486 : browser.BrowserDriver.FindElements(By.CssSelector($"[placeholder='{selectorPath.SelectorPath}']"));
1487
1488 GPALelems = new ReadOnlyCollection<GPALElement>(
1489 elems.Select(e => new GPALElement(e, selector.AttributeName) { Browser = browser, Selector = selector }).ToList()
1490 );
1491 }
1492
1493 return GPALelems;
1494 }
1495
1505 public static void PublishToEventHandler(Browser browser, Selector selector, List<GPALElement> webElements)
1506 {
1507 string msg = "";
1508 if (true == selector.DeleteMe)
1509 msg = $"Ignoring selector [{selector.Name}] marked for deletion. Continuing.";
1510 else if (null == webElements || 0 == webElements.Count)
1511 msg = $"No elements found for selector [{selector.Name}]. Continuing.";
1512
1513 GPAL.PublishSimpleEvent(GPALEventType.WARNING, msg, browser, GPALObjectType.Browser);
1514 }
1525 public static List<GPALElement> WaitFor(Browser browser, int timeoutInTicks, out bool matchedAll)
1526 {
1527 bool matchedAll2 = true;
1528 List<GPALElement> foundElements = null;
1529 List<GPALElement> matchedElements;
1530
1531 foundElements = new List<GPALElement>();
1532
1533 foreach (Selector selector in browser.CurrentUOW.WithSelectorList)
1534 {
1535 if (SelectorType.Selector != selector.SelectorType)
1536 continue; // we don't look up data literals
1537
1538 WaitFor(browser, selector, timeoutInTicks, out matchedAll2, out matchedElements);
1539
1540
1541 // FindWebElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
1542 if (true == selector.DeleteMe || null == matchedElements)
1543 {
1544 PublishToEventHandler(browser, selector, matchedElements);
1545 continue;
1546 }
1547
1548 int rowCount = 0;
1549
1550 foreach (GPALElement webElement in matchedElements)
1551 {
1552 foundElements.Add(webElement);
1553
1554 if (++rowCount >= browser.CurrentUOW.WithAllThatMatch)
1555 break;
1556 }
1557 }
1558
1559 matchedAll = matchedAll2;
1560 return foundElements;
1561 }
1575 private static List<GPALElement> WaitFor(Browser browser, Selector selector, int timeoutInTicks, out bool matchedAll, out List<GPALElement> matchedElements, GPALElement webElement = null)
1576 {
1577 bool matchedAll2 = true;
1578 List<GPALElement> matchedElements2;
1579 ReadOnlyCollection<GPALElement> elements;
1580 int orgTimeoutInTicks = timeoutInTicks;
1581
1582 if (WaitTime.Forever == timeoutInTicks)
1583 timeoutInTicks = Int32.MaxValue; // forever
1584
1585 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Waiting up to [{timeoutInTicks}] ms for elements matching selector [{selector.Name}][{selector.SelectorPath}]", browser, GPALObjectType.Browser);
1586
1587 var sw = Stopwatch.StartNew();
1588 do
1589 {
1590 elements = FindWebElements(browser, browser.CurrentUOW, selector,
1591 out matchedAll2, out matchedElements2, webElement, false);
1592
1593 // we might've run out of time already and we could generate a negative sleep time
1594 if (0 == matchedElements2?.Count && WaitTime.Immediate != timeoutInTicks && sw.ElapsedMilliseconds < timeoutInTicks)
1595 Thread.Sleep((int)Math.Min(1000, timeoutInTicks - sw.ElapsedMilliseconds));
1596
1597 } while (sw.ElapsedMilliseconds < timeoutInTicks &&
1598 (matchedElements2 == null || matchedElements2.Count == 0));
1599
1600 matchedAll = matchedAll2; // one of those found it or not :)
1601 matchedElements = matchedElements2; // always use matched, it's either all or those matched
1602
1603 // element found, did we set a waitfor state as well?
1604 if (ElementState.NotSet != browser.CurrentUOW.WaitForElementState)
1605 foreach (GPALElement gPALElement in matchedElements2) // will be all if no match set
1606 matchedAll &= WaitFor(browser, gPALElement, browser.CurrentUOW.WaitForElementState, orgTimeoutInTicks);
1607
1608 return elements?.ToList<GPALElement>();
1609 }
1610
1619 public static bool WaitFor(Browser browser, GPALElement element, ElementState state, int timeoutInTicks)
1620 {
1621 const int defaultStateWaitInTicks = 3000;
1622
1623 if (WaitTime.Forever == timeoutInTicks)
1624 {
1625 timeoutInTicks = Int32.MaxValue;
1626 }
1627 else if (timeoutInTicks < 0)
1628 {
1629 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"WaitFor(ElementState.[{state}]) needs a positive wait time but got [{timeoutInTicks}] ms (no .WaitFor(timeInMs) was set for this unit of work) - using the default of [{defaultStateWaitInTicks}] ms instead.", element, GPALObjectType.Other);
1630 timeoutInTicks = defaultStateWaitInTicks;
1631 }
1632
1633 int retries = 2;
1634 bool retry = true;
1635 dynamic retryElement = element;
1636 int tmpTimeoutInTicks = timeoutInTicks;
1637
1638 do
1639 {
1640 try
1641 {
1642 int sleepTime = Math.Min(1000, tmpTimeoutInTicks);
1643
1644 Thread.Sleep(sleepTime);
1645
1646 if (IsElementInState(browser, element, state))
1647 return true;
1648
1649 tmpTimeoutInTicks -= sleepTime;
1650 }
1651 catch (StaleElementReferenceException sereex)
1652 {
1653 string message = $"StaleElementReferenceException while waiting for state [{state}]: [{sereex.Message}]";
1654 retries--;
1655 retry = ResolveWebElementIssue(browser, browser.CurrentUOW, ref retryElement, sereex, ref retries, message);
1656 if (retry)
1657 element = retryElement;
1658 }
1659 catch (GPALException)
1660 {
1661 throw;
1662 }
1663 catch (Exception ex)
1664 {
1665 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Error while waiting for state [{state}]", element, GPALObjectType.Other, ex);
1666 retry = false;
1667 }
1668 } while (tmpTimeoutInTicks > 0 && retry);
1669
1670 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Element not in state [{state}] within [{timeoutInTicks}] ms.", element, GPALObjectType.Other);
1671 return false;
1672 }
1673
1681 private static bool IsElementInState(Browser browser, GPALElement element, ElementState state)
1682 {
1683 try
1684 {
1685 switch (state)
1686 {
1687 case ElementState.Visible:
1688 return element.Displayed;
1689 case ElementState.Hidden:
1690 return !element.Displayed;
1691 case ElementState.Enabled:
1692 return element.Enabled;
1693 case ElementState.Disabled:
1694 return !element.Enabled;
1695 case ElementState.Checked:
1696 return element.Selected || element.GetAttribute("checked") == "true";
1697 case ElementState.Unchecked:
1698 return !element.Selected && element.GetAttribute("checked") != "true";
1699 case ElementState.Selected:
1700 return element.Selected;
1701 case ElementState.Unselected:
1702 return !element.Selected;
1703 case ElementState.Clickable:
1704 return IsElementClickable(element);
1705 case ElementState.Editable:
1706 return IsElementEditable(element);
1707 default:
1708 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unsupported element state [{state}].", element, GPALObjectType.Other);
1709 return false;
1710 }
1711 }
1712 catch (GPALException)
1713 {
1714 throw;
1715 }
1716 catch (Exception ex)
1717 {
1718 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Error checking state [{state}]", element, GPALObjectType.Other, ex);
1719 return false;
1720 }
1721 }
1729 public static bool FillInFrom(Browser browser, string useText, WriteMode writeMode, GPALElement element = null)
1730 {
1731 bool retVal = false; // did we fill in anything? useful for SendString to try this before sending to <body>
1732 ReadOnlyCollection<GPALElement> elems = null;
1733 List<GPALElement> matchedElements = null;
1734
1735 foreach (Selector selector in browser.CurrentUOW.WithSelectorList)
1736 {
1737 if (SelectorType.Selector != selector.SelectorType)
1738 continue; // we don't look up data literals
1739
1740 if (null == element)
1741 elems = ElementHelper.FindWebElements(browser, browser.CurrentUOW, selector, out bool matchedAll, out matchedElements);
1742
1743 // FindWebElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
1744 if (true == selector.DeleteMe || null == elems)
1745 {
1746 PublishToEventHandler(browser, selector, elems?.ToList());
1747 continue;
1748 }
1749
1750 if (null != element)
1751 {
1752 var tmpElems = new List<GPALElement>();
1753 tmpElems.Add(element);
1754 elems = new ReadOnlyCollection<GPALElement>(tmpElems);
1755 }
1756 else if (null != matchedElements)
1757 elems = new ReadOnlyCollection<GPALElement>(matchedElements);
1758
1759 if (true == elems.Any())
1760 handleFillIn(elems);
1761
1762 if (true == retVal && null != browser.CurrentUOW.CallAfterFillIn && true == elems.Any())
1763 {
1764 UnitOfWork safeUOW = browser.CurrentUOW;
1765 CallIfStatus handled = 0;
1766
1767 // CAVEAT: there is no concept of 'handled (1)' vs 'not handled (0)' but definitely can request to exit
1768 // call CallAfterFillIn, if defined and pass in the tokens we are currently using (which is just a string in this method)
1769 IGPALGrid<string> tokens = GPAL.GridForType<string>();
1770 tokens.AddRow(new List<string> { useText });
1771
1772 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Invoking CallAfterFillIn [{browser.CurrentUOW.CallAfterFillIn.Method.Name}]", browser, GPALObjectType.Browser);
1773 handled = browser.CurrentUOW.CallAfterFillIn(browser, tokens, 1);
1774
1775 // CAVEAT: kludge - the call after handler can and probably will set a new current unit of work, but we need our old current unit
1776 // the call after handler UOW is no longer in scope, so restore our UOW
1777 browser.CurrentUOW = safeUOW;
1778
1779 // CAVEAT: elements may go stale because of callafterfillin handler actions
1780 // test if elements still valid by testing the first one
1781 //try
1782 //{
1783 // ((UnitOfWork.ElementNode)(browser.CurrentUOW.ElementGrid[0])[0]).GPALElement.GetAttribute("tag");
1784 //}
1785 //catch // failed, invalidate elementgrid
1786 //{
1787 // browser.CurrentUOW.ElementGrid.Clear();
1788 //}
1789
1790 if (CallIfStatus.Terminate == handled)
1791 {
1792 string str = $"CallAfterFillIn handler [{browser.CurrentUOW.AppCallAfterFillIn.GetInvocationList()[0].Method.Name}] requested program termination.";
1793 GPAL.PublishSimpleEvent(GPALEventType.INFO, str, browser, GPALObjectType.Browser);
1794 throw new GPALException($"" + str);
1795 }
1796 }
1797
1798 void handleFillIn(ReadOnlyCollection<GPALElement> elems)
1799 {
1800 int rowCount = 0;
1801
1802 foreach (GPALElement elem in elems)
1803 {
1804 string type = elem.GetAttribute("type");
1805 string abbreviatedText = useText[0] + "..." + useText[useText.Length - 1];
1806
1807 if (null == type || false == type.Equals("password"))
1808 {
1809 if (GPALEventType.DEBUG == (GPALEventType.DEBUG & GPAL.GPALSettings.DebugEvents) || GPALEventType.DEBUG == (GPALEventType.DEBUG & GPAL.GPALSettings.ConsoleEvents))
1810 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"[{writeMode}] text [{useText}] in element [{elem.TagName}][{selector.Name}]", browser, GPALObjectType.Browser);
1811 else
1812 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{writeMode}] text [{abbreviatedText}] in element [{elem.TagName}][{selector.Name}]", browser, GPALObjectType.Browser);
1813 }
1814 else
1815 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{writeMode}] XXXpasswordXXX in element [{elem.TagName}][{selector.Name}]", browser, GPALObjectType.Browser);
1816
1817 if (null != elem)
1818 {
1819 retVal = true;
1820
1821 if (false == browser.BrowserSettings.UseHeadless
1822 && ("GPALElement" == elem.TagName // image match - coordinates only, no handle any engine can focus
1823 || true == browser.BrowserSettings.UseHardware || true == GPAL.GPALSettings.UseHardware))
1824 HardwareFillInFrom(browser, elem, selector.OffsetX, selector.OffsetY, useText, writeMode);
1825 else if ("GPALElement" == elem.TagName)
1826 // headless image match - no handle for any engine, focus the coordinate and type
1827 FillInFrom(browser, ToViewportPoint(browser, elem, selector), useText, writeMode);
1828 else if (browser.UseOttoMagic)
1829 {
1830 if (false == browser.BrowserSettings.UseHeadless && InteractionType.Hardware == selector.InteractionType)
1831 ElementHelper.HardwareFillInFrom(browser, elem, selector.OffsetX, selector.OffsetY, useText, writeMode);
1832 else
1833 switch (writeMode)
1834 {
1835 case WriteMode.Append:
1836 browser.MagicHelper.FillInAppend(elem.Css, useText);
1837 break;
1838 case WriteMode.Insert:
1839 browser.MagicHelper.FillInInsert(elem.Css, useText);
1840 break;
1841 case WriteMode.Overwrite:
1842 browser.MagicHelper.FillInOverwrite(elem.Css, useText);
1843 break;
1844 }
1845
1846 // browser.MagicHelper.FireChangeEvent(elem.Css);
1847
1848 if (++rowCount >= browser.CurrentUOW.WithAllThatMatch)
1849 break;
1850 }
1851 else if (true == browser.UsePuppeteer)
1852 {
1853 if (false == browser.BrowserSettings.UseHeadless && InteractionType.Hardware == selector.InteractionType)
1854 ElementHelper.HardwareFillInFrom(browser, elem, selector.OffsetX, selector.OffsetY, useText, writeMode);
1855 else
1856 switch (writeMode)
1857 {
1858 case WriteMode.Append:
1859 browser.PuppeteerClient.FillInAppend(elem).WithText(useText).Execute();
1860 break;
1861 case WriteMode.Insert:
1862 browser.PuppeteerClient.FillInInsert(elem).WithText(useText).Execute();
1863 break;
1864 case WriteMode.Overwrite:
1865 browser.PuppeteerClient.FillInOverwrite(elem).WithText(useText).Execute();
1866 break;
1867 }
1868
1869 // browser.PuppeteerClient.FireChangeEvent(elem.ElementHandle).Execute();
1870
1871 if (++rowCount >= browser.CurrentUOW.WithAllThatMatch)
1872 break;
1873 }
1874 else if (true == browser.UseSelenium)
1875 {
1876 if (false == browser.BrowserSettings.UseHeadless && InteractionType.Hardware == selector.InteractionType)
1877 ElementHelper.HardwareFillInFrom(browser, elem, selector.OffsetX, selector.OffsetY, useText, writeMode);
1878 else
1879 SeleniumFillInFrom(browser, elem, useText, writeMode);
1880 }
1881 }
1882 }
1883 }
1884 }
1885
1886 return retVal;
1887 }
1895 public static void SeleniumSendKeys(Browser browser, GPALElement element, byte keyCode)
1896 {
1897 // Map the byte keycode to Selenium Keys constants
1898 string keyToSend;
1899
1900 switch (keyCode)
1901 {
1902 case 0x12: // VK_ALT
1903 keyToSend = Keys.Alt;
1904 break;
1905 case 0x2e: // VK_DELETE
1906 keyToSend = Keys.Delete;
1907 break;
1908 case 0x79: // VK_F10
1909 keyToSend = Keys.F10;
1910 break;
1911 case 0x21: // VK_PRIOR
1912 keyToSend = Keys.PageUp;
1913 break;
1914 case 0x22: // VK_NEXT
1915 keyToSend = Keys.PageDown;
1916 break;
1917 case 0x23: // VK_END
1918 keyToSend = Keys.End;
1919 break;
1920 case 0x24: // VK_HOME
1921 keyToSend = Keys.Home;
1922 break;
1923 case 0x28: // VK_DOWN
1924 keyToSend = Keys.ArrowDown;
1925 break;
1926 case 0x09: // VK_TAB
1927 keyToSend = Keys.Tab;
1928 break;
1929 case 0x0D: // VK_RETURN
1930 keyToSend = Keys.Return;
1931 break;
1932 case 0x27: // VK_SHIFT_RIGHT
1933 keyToSend = Keys.Shift;
1934 break;
1935 case 0xA0: // VK_SHIFT_LEFT
1936 keyToSend = Keys.LeftShift;
1937 break;
1938 case 0xA2: // VK_CONTROL_LEFT
1939 keyToSend = Keys.LeftControl;
1940 break;
1941 case 0xA3: // VK_CONTROL_RIGHT
1942 keyToSend = Keys.Control;
1943 break;
1944 case 0x20: // VK_SPACE
1945 keyToSend = Keys.Space;
1946 break;
1947 case 0x08: // VK_BACK
1948 keyToSend = Keys.Backspace;
1949 break;
1950 default:
1951 // If it's a character key, use fromCharCode method for alphanumeric keys
1952 keyToSend = ((char)keyCode).ToString();
1953 break;
1954 }
1955
1956 // Find the element to send keys to
1957 element.SendKeys(keyToSend);
1958 }
1959
1969 // the input types the browser renders as a widget rather than a text box. Typing into one means matching
1970 // that browser's segment order, separators and tab behavior, which differ between chrome, edge and
1971 // firefox and change with locale. Every one of them accepts the ISO value written to .value, which is what
1972 // OttoMagic and Puppeteer have always done, so Selenium does it that way too
1973 private static readonly HashSet<string> valueOnlyInputTypes = new HashSet<string>
1974 {
1975 "color", "date", "datetime-local", "month", "week", "time",
1976 };
1977 public static void SeleniumFillInFrom(Browser browser, GPALElement elem, string textToUse, WriteMode writeMode)
1978 {
1979 // the ISO value goes to .value untouched - GetKeystrokeValueForInput exists to reshape it for typing
1980 if (true == valueOnlyInputTypes.Contains(elem.Type))
1981 {
1982 JavaScriptFillInFrom(browser, elem, textToUse, writeMode);
1983 return;
1984 }
1985
1986 textToUse = ElementHelper.GetKeystrokeValueForInput(elem, textToUse);
1987
1988 try
1989 {
1990 if (WriteMode.Overwrite == writeMode)
1991 elem.Clear();
1992 else if (WriteMode.Append == writeMode)
1993 {
1994 textToUse = elem.Text + textToUse;
1995 elem.Clear();
1996 }
1997
1998 elem.SendKeys(textToUse); // fast path - whole string at once
1999 }
2000 catch (GPALException)
2001 {
2002 throw;
2003 }
2004 catch (Exception ex)
2005 {
2006 if (false == GPAL.NoFallbackRecoveryActions)
2007 {
2008 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, "Unable to fill in text. Falling back on javascript.", browser, GPALObjectType.Browser, ex);
2009 JavaScriptFillInFrom(browser, elem, textToUse, writeMode);
2010 }
2011 }
2012
2013 }
2014
2015
2025 public static void HardwareFillInFrom(Browser browser, GPALElement webElement, int offsetX, int offsetY, string text, WriteMode writeMode)
2026 {
2027 string inputType = webElement.Type?.ToLowerInvariant() ?? "";
2028
2029 // Special handling for native picker inputs
2030 bool isSpecialInput = new[] { "date", "time", "datetime-local", "month", "week", "color" }
2031 .Contains(inputType);
2032
2033 HardwareFocus(browser, webElement, offsetX, offsetY);
2034
2035 // If focusing the element could not be trusted (off-screen/odd coords, so no real hardware click
2036 // happened), do NOT type. Focus is likely still on whatever it was before - e.g. the browser's
2037 // address bar - and blasting keystrokes there is how a search term ends up in the URL bar.
2038 if (true == _lastPlacementQuestionable)
2039 {
2040 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
2041 $"Skipping fill-in [{browser.CurrentUOW?.CurrentSelector?.Name}], questionable element placement.",
2042 browser, GPALObjectType.Browser);
2043 return;
2044 }
2045
2046 if (isSpecialInput)
2047 {
2048 // firefox does not consume Ctrl+A inside a segmented date or time widget, it hands it to the
2049 // document and the whole page ends up selected. the segments overwrite as they are typed anyway,
2050 // so the clear is only there for the browsers that want it
2051 if ("week" != inputType && BrowserType.FireFox != browser.BrowserSettings.BrowserType)
2052 {
2053 // Aggressive clear for special inputs (they can be picky)
2054 HardwareHelper.SendChar('a', ModifierKeys.Control, true);
2055 Thread.Sleep(120);
2056 HardwareHelper.SendKey(GPAL.VK_DELETE);
2057 Thread.Sleep(80);
2058 }
2059 }
2060 else if (WriteMode.Overwrite == writeMode)
2061 {
2062 HardwareHelper.SendChar('a', ModifierKeys.Control, true);
2063 Thread.Sleep(150);
2064 //HardwareHelper.SendKey(GPAL.VK_DELETE);
2065 }
2066 else if (WriteMode.Append == writeMode)
2067 {
2068 HardwareHelper.SendKey(GPAL.VK_END, ModifierKeys.Control);
2069 Thread.Sleep(150);
2070 }
2071 else if (WriteMode.Insert == writeMode)
2072 {
2073 HardwareHelper.SendKey(GPAL.VK_HOME, ModifierKeys.Control);
2074 Thread.Sleep(150);
2075 }
2076
2077 string textToType = GetKeystrokeValueForInput(webElement, text);
2078 switch (inputType)
2079 {
2080 case "color":
2081 // the picker takes the six hex digits, not the css value, and anything else falls back to black
2082 HardwareFillInColor(textToType.TrimStart('#'), browser.BrowserSettings.BrowserType);
2083 break;
2084 case "datetime-local":
2085 HardwareFillInDateTimeLocal(textToType, browser.BrowserSettings.BrowserType);
2086 break;
2087 case "week":
2088 HardwareFillInWeek(textToType);
2089 break;
2090 case "month":
2091 HardwareFillInMonth(textToType);
2092 break;
2093 default:
2094 HardwareHelper.SendString(textToType);
2095 break;
2096 }
2097 }
2098
2103 public static void HardwareFillInMonth(string isoValue)
2104 {
2105 var parts = isoValue.Split('/');
2106 string year = parts[0];
2107 string month = parts[1];
2108 HardwareHelper.SendString(month); // "12"
2109 // TAB to move to year field
2110 HardwareHelper.SendKey(GPAL.VK_TAB);
2111 HardwareHelper.SendString(year); // "2026"
2112 }
2113
2118 public static void HardwareFillInWeek(string isoValue)
2119 {
2120 if (isoValue.Contains("-W"))
2121 {
2122 var parts = isoValue.Split('-');
2123 string year = parts[0];
2124 string week = parts[1].TrimStart('W');
2125 HardwareHelper.SendString($"{week}{year}"); // "31 2026"
2126 }
2127 }
2128
2133 public static void HardwareFillInDateTimeLocal(string isoValue, BrowserType browserType)
2134 {
2135 // Convert ISO "2026-06-18T18:32" → US human format "06/18/2026 06:32 PM"
2136 if (!DateTime.TryParseExact(isoValue,
2137 "MM/dd/yyyy HH:mm",
2138 null,
2139 System.Globalization.DateTimeStyles.None,
2140 out DateTime dt))
2141 {
2142 dt = DateTime.Now; // fallback
2143 }
2144
2145 string datePart = dt.ToString("MM/dd/yyyy"); // US date
2146 string timePart = dt.ToString("hh:mm"); // 06:32 pm (lowercase a/p)
2147 string amPm = dt.ToString("tt");
2148
2149 // Type date
2150 HardwareHelper.SendString(datePart);
2151
2152 // firefox advances to the hour on its own once the year is typed, so tabbing here moves off the hour
2153 // and the time lands a segment late. the chromium widget still wants the tab
2154 if (BrowserType.FireFox != browserType)
2155 {
2156 // TAB to move to time field
2157 HardwareHelper.SendKey(GPAL.VK_TAB);
2158 Thread.Sleep(120);
2159 }
2160
2161 // Type time in 12-hour format
2162 HardwareHelper.SendString(timePart);
2163 Thread.Sleep(120);
2164
2165 // am/pm
2166 HardwareHelper.SendString(amPm);
2167 }
2168
2175 public static void HardwareFillInColor(string hexValue, BrowserType browserType)
2176 {
2177 if (hexValue.Length != 6)
2178 {
2179 // fallback or error
2180 hexValue = "000000";
2181 }
2182
2183 // Convert #RRGGBB → R, G, B decimals
2184 int r = Convert.ToInt32(hexValue.Substring(0, 2), 16);
2185 int g = Convert.ToInt32(hexValue.Substring(2, 2), 16);
2186 int b = Convert.ToInt32(hexValue.Substring(4, 2), 16);
2187
2188 // Navigate into the RGB fields. Three tabs reaches the first one in the chromium picker. Firefox lists
2189 // hue, saturation and luminance ahead of red, green and blue, so it takes four more to clear them
2190 int tabsToFirstField = BrowserType.FireFox == browserType ? 7 : 3;
2191
2192 for (int tab = 0; tab < tabsToFirstField; tab++)
2193 HardwareHelper.SendKey(GPAL.VK_TAB);
2194
2195 // Enter R
2196 HardwareHelper.SendString(r.ToString());
2197 HardwareHelper.SendKey(GPAL.VK_TAB);
2198
2199 // Enter G
2200 HardwareHelper.SendString(g.ToString());
2201 HardwareHelper.SendKey(GPAL.VK_TAB);
2202
2203 // Enter B
2204 HardwareHelper.SendString(b.ToString());
2205
2206 // Optional: Tab out + Enter to confirm / close picker
2207 //HardwareHelper.SendKey(GPAL.VK_TAB);
2208 HardwareHelper.SendKey(GPAL.VK_RETURN); // or VK_ENTER
2209 }
2216 public static string GetKeystrokeValueForInput(GPALElement webElement, string isoValue)
2217 {
2218 if (string.IsNullOrEmpty(isoValue) || webElement?.TagName?.ToLowerInvariant() != "input")
2219 return isoValue;
2220
2221 string type = webElement.Type?.ToLowerInvariant() ?? "";
2222
2223 try
2224 {
2225 switch (type)
2226 {
2227 case "date":
2228 // ISO: 2026-06-18 US typing: 06/18/2026
2229 if (DateTime.TryParseExact(isoValue, "yyyy-MM-dd", null, System.Globalization.DateTimeStyles.None, out DateTime dt))
2230 return dt.ToString("MM/dd/yyyy");
2231 return isoValue;
2232
2233 case "datetime-local":
2234 // ISO: 2026-06-18T18:32 US: 06/18/2026 18:32
2235 if (DateTime.TryParseExact(isoValue.Replace("T", " "), "yyyy-MM-dd HH:mm", null, System.Globalization.DateTimeStyles.None, out dt))
2236 return dt.ToString("MM/dd/yyyy HH:mm");
2237 return isoValue.Replace("T", " ");
2238
2239 case "month":
2240 // ISO: 2026-12 12/2026 (most browsers accept this when typing)
2241 return isoValue.Replace("-", "/");
2242
2243 case "week":
2244 // 2026-W31 keep as-is or try "31 2026" — Wxx format is usually required
2245 // Many browsers expect the ISO week format even when typing
2246 return isoValue; // safest starting point
2247
2248 case "time":
2249 // the value is HH:mm on its own, and the widget types a 12 hour clock with a meridiem
2250 // segment, so am/pm is worked out from the 24 hour value rather than supplied
2251 if (DateTime.TryParseExact(isoValue, new[] { "HH:mm", "H:mm", "HH:mm:ss" }, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out dt))
2252 return dt.ToString("hh:mm tt", System.Globalization.CultureInfo.InvariantCulture);
2253 return isoValue;
2254
2255 default:
2256 return isoValue;
2257 }
2258 }
2259 catch
2260 {
2261 return isoValue; // fallback
2262 }
2263 }
2264
2271 public static void JavaScriptFillInFrom(Browser browser, GPALElement webElement, string text, WriteMode writeMode)
2272 {
2273 IWebDriver browserDriver = browser.BrowserDriver;
2274
2275 BrowserHelper.ExecuteJavaScript(browser, "arguments[0].focus();", webElement.WebElement);
2276
2277 // .value, not setAttribute('value'). The attribute is the field's default value: once anything has been
2278 // typed into the field the browser submits the property, so writing the attribute changes what the page
2279 // looks like and not what it sends
2280 if (WriteMode.Overwrite == writeMode)
2281 BrowserHelper.ExecuteJavaScript(browser, "arguments[0].value = arguments[1]", webElement.WebElement, text);
2282 else if (WriteMode.Append == writeMode)
2283 BrowserHelper.ExecuteJavaScript(browser, "arguments[0].value = arguments[0].value + arguments[1]", webElement.WebElement, text);
2284 else if (WriteMode.Insert == writeMode)
2285 BrowserHelper.ExecuteJavaScript(browser, "arguments[0].value = arguments[1] + arguments[0].value", webElement.WebElement, text);
2286
2287 // typing raises these, so a page that keeps its own copy of the field only hears about the new text if
2288 // we raise them too. Without it the page keeps acting on whatever was in the box before
2289 // todo: make firing this an optional param
2290 BrowserHelper.ExecuteJavaScript(browser,
2291 "arguments[0].dispatchEvent(new Event('input', { bubbles: true })); arguments[0].dispatchEvent(new Event('change', { bubbles: true }));",
2292 webElement.WebElement);
2293 }
2300 public static async void PuppeteerFillInFrom(Browser browser, GPALElement webElement, string text, WriteMode writeMode)
2301 {
2302 BrowserSettings browserSettings = browser.BrowserSettings;
2303
2304 PuppeteerCommunicator puppeteerCommunicator = browserSettings.PuppeteerCommunicator;
2305
2306 // https://stackoverflow.com/questions/11337353/correct-way-to-focus-an-element-in-selenium-webdriver-using-java
2307 // puppeteerCommunicator.Focus(webElement.Css);
2308 switch (writeMode)
2309 {
2310 case WriteMode.Append:
2311 browser.PuppeteerClient.FillInAppend(webElement).WithText(text).Execute();
2312 break;
2313 case WriteMode.Insert:
2314 browser.PuppeteerClient.FillInInsert(webElement).WithText(text).Execute();
2315 break;
2316 case WriteMode.Overwrite:
2317 browser.PuppeteerClient.FillInOverwrite(webElement).WithText(text).Execute();
2318 break;
2319 }
2320 VkCodeToDomKeyConverter.TryConvertAsciiToVkCode('\t', out byte vkCode);
2321 VkCodeToDomKeyConverter.TryConvertVkCodeToDomKey(vkCode, out string key, out string code);
2322 await puppeteerCommunicator.SendKey(key, code, vkCode); // tab out, force change event to fire - todo: make firing this an optional param
2323 }
2324
2332 public static void JavaScriptSendKey(Browser browser, GPALElement webElement, byte keycode)
2333 {
2334 IWebDriver browserDriver = browser.BrowserDriver;
2335 if (true == VkCodeToDomKeyConverter.TryConvertVkCodeToDomKey(keycode, out string key, out string code))
2336 {
2337 string sendKeyScript = @"
2338 function isElementEditable(target) {
2339 if (!target || !(target instanceof Element)) return false;
2340 if (target.disabled || target.readOnly) return false;
2341
2342 const tag = target.tagName.toUpperCase();
2343 if (tag === 'TEXTAREA') return true;
2344 if (tag === 'SELECT') return true;
2345 if (tag === 'INPUT') {
2346 const editableTypes = new Set(['text','email','password','search','number','url','tel',
2347 'date','datetime-local','month','time','week','color']);
2348 return editableTypes.has(target.type.toLowerCase());
2349 }
2350 if (target.isContentEditable === true ||
2351 target.getAttribute('contenteditable') === 'true' ||
2352 target.ownerDocument?.designMode === 'on') {
2353 return true;
2354 }
2355 const style = window.getComputedStyle(target);
2356 if (style.userModify === 'read-write' || style.webkitUserModify === 'read-write' ||
2357 style.cursor === 'text') {
2358 return true;
2359 }
2360 return false;
2361 }
2362
2363 // === Parameters passed from C# ===
2364 const key = arguments[0]; // e.g. 'a', 'Enter', 'Tab'
2365 const code = arguments[1]; // e.g. 'KeyA', 'Enter'
2366 const keyCode = arguments[2]; // numeric keyCode if needed (mostly legacy)
2367
2368 let target = document.activeElement;
2369 if (!target || target === document.body) {
2370 target = document; // fallback
2371 }
2372
2373 const eventInit = {
2374 key: key,
2375 code: code,
2376 keyCode: keyCode,
2377 bubbles: true,
2378 cancelable: true,
2379 composed: true
2380 };
2381
2382 try {
2383 // 1. Key events (for listeners)
2384 target.dispatchEvent(new KeyboardEvent('keydown', eventInit));
2385
2386 if (key.length === 1) { // printable char
2387 target.dispatchEvent(new KeyboardEvent('keypress', eventInit));
2388 }
2389
2390 // 2. Actually insert text (this is what most sites need)
2391 const isEditable = isElementEditable(target);
2392 if (isEditable) {
2393 if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
2394 const start = target.selectionStart ?? target.value.length;
2395 const end = target.selectionEnd ?? target.value.length;
2396 target.value = target.value.slice(0, start) + key + target.value.slice(end);
2397 target.selectionStart = target.selectionEnd = start + key.length;
2398 }
2399 else if (target.isContentEditable) {
2400 // Simpler fallback for contenteditable
2401 document.execCommand('insertText', false, key); // still works in many places
2402 }
2403
2404 // Fire input event (React, Vue, Angular, etc. listen to this)
2405 target.dispatchEvent(new InputEvent('input', {
2406 bubbles: true,
2407 cancelable: true,
2408 data: key,
2409 inputType: 'insertText',
2410 composed: true
2411 }));
2412
2413 // Also fire change for some frameworks
2414 target.dispatchEvent(new Event('change', { bubbles: true }));
2415 }
2416
2417 // 3. Keyup
2418 target.dispatchEvent(new KeyboardEvent('keyup', eventInit));
2419
2420 // Special case: Enter on button/submit
2421 if (key === 'Enter' && (target.tagName === 'BUTTON' ||
2422 (target.tagName === 'INPUT' && (target.type === 'submit' || target.type === 'button')))) {
2423 target.click();
2424 }
2425
2426 } catch (e) {
2427 console.error('SendKey script error:', e);
2428 }
2429 ";
2430
2431 // Call it like this:
2432 BrowserHelper.ExecuteJavaScript(browser, sendKeyScript, key, code, keycode.ToString()); // example for 'A'
2433 }
2434 }
2443 public static void Focus(Browser browser)
2444 {
2445 List<GPALElement> matchedElements;
2446
2447 foreach (Selector selector in browser.CurrentUOW.WithSelectorList)
2448 {
2449 if (SelectorType.Selector != selector.SelectorType)
2450 continue; // we don't look up data literals
2451
2452 ElementHelper.FindWebElements(browser, browser.CurrentUOW, selector, out bool matchedAll, out matchedElements);
2453
2454 // FindWebElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
2455 if (true == selector.DeleteMe || null == matchedElements)
2456 {
2457 PublishToEventHandler(browser, selector, matchedElements?.ToList());
2458 continue;
2459 }
2460
2461 int rowCount = 0;
2462
2463 foreach (GPALElement elem in matchedElements)
2464 {
2465 if (null != elem)
2466 {
2467 if (false == browser.BrowserSettings.UseHeadless && (true == browser.BrowserSettings.UseHardware || true == GPAL.GPALSettings.UseHardware))
2468 {
2469 HardwareFocus(browser, elem, selector.OffsetX, selector.OffsetY);
2470 }
2471 else if (true == IsElementEditable(elem))
2472 {
2473 // click to focus and place the caret, readying the element for typing
2474 Click(browser, selector, elem, ClickType.LeftClick, ModifierKeys.NONE);
2475 }
2476 else if (true == browser.UseOttoMagic)
2477 {
2478 browser.MagicHelper.Focus(elem.Css);
2479 }
2480 else if (true == browser.UsePuppeteer)
2481 {
2482 browser.PuppeteerClient.Focus(elem.Css).Execute();
2483 }
2484 else if (true == browser.UseSelenium)
2485 {
2486 if (Enums.InteractionType.JavaScript == selector.InteractionType || true == browser.BrowserSettings.UseJavaScript) // selenium
2487 {
2488 JavaScriptFocus(browser, elem);
2489 }
2490 else if (elem.TagName.ToLower().Equals("input"))
2491 {
2492 elem.SendKeys(OpenQA.Selenium.Keys.Shift);
2493 elem.SendKeys("");
2494 }
2495 else
2496 {
2497 try
2498 {
2499 new Actions(browser.BrowserDriver).MoveToElement((IWebElement)elem.WebElement).Perform();
2500 }
2501 catch (GPALException)
2502 {
2503 throw;
2504 }
2505 catch (Exception ex)
2506 {
2507 if (false == GPAL.NoFallbackRecoveryActions)
2508 {
2509 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Selenium threw an excaption on selector [{selector.Name}], falling back to javascript.", browser, GPALObjectType.Browser, ex);
2510 ElementHelper.ScrollIntoView(browser, elem);
2511 }
2512 else
2513 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Selenium threw an excaption on selector [{selector.Name}], continuing.", browser, GPALObjectType.Browser, ex);
2514 }
2515 }
2516 }
2517
2518 if (++rowCount >= browser.CurrentUOW.WithAllThatMatch)
2519 break;
2520 }
2521 }
2522 }
2523 }
2530 private static void JavaScriptFocus(Browser browser, GPALElement webElement)
2531 {
2532 IWebDriver browserDriver = browser.BrowserDriver;
2533
2534 string forceFocusScript = $@"
2535 function forceFocus(el) {{
2536 // Remember original state
2537 const wasDisabled = el.disabled;
2538 const originalTabIndex = el.getAttribute('tabindex');
2539
2540 try {{
2541 // 1. Enable if disabled
2542 if (wasDisabled) el.disabled = false;
2543
2544 // 2. Make sure it can receive focus
2545 if (el.tabIndex === -1 || !el.hasAttribute('tabindex')) {{
2546 el.setAttribute('tabindex', '0');
2547 }}
2548
2549 // 3. Focus it
2550 el.focus({{ preventScroll: true }});
2551
2552 // 4. Small delay + aggressive fallback (helps with buttons, React, Shadow DOM, etc.)
2553 setTimeout(() => {{
2554 if (document.activeElement !== el) {{
2555 el.focus();
2556
2557 // Dispatch synthetic focus events
2558 el.dispatchEvent(new FocusEvent('focusin', {{ bubbles: true }}));
2559 el.dispatchEvent(new FocusEvent('focus', {{ bubbles: false }}));
2560 }}
2561 }}, 0);
2562
2563 // Final check
2564 return document.activeElement === el;
2565
2566 }} catch (err) {{
2567 return false;
2568 }} finally {{
2569 // Restore original state
2570 if (wasDisabled) el.disabled = true;
2571 if (originalTabIndex !== null) {{
2572 el.setAttribute('tabindex', originalTabIndex);
2573 }} else if (el.getAttribute('tabindex') === '0') {{
2574 el.removeAttribute('tabindex');
2575 }}
2576 }}
2577 }}
2578 forceFocus(arguments[0]);
2579 ";
2580 BrowserHelper.ExecuteJavaScript(browser, forceFocusScript, webElement.WebElement); // focus on element
2581 }
2586 public static void Hide(Browser browser)
2587 {
2588 List<GPALElement> matchedElements;
2589
2590 foreach (Selector selector in browser.CurrentUOW.WithSelectorList)
2591 {
2592 if (SelectorType.Selector != selector.SelectorType)
2593 continue; // we don't look up data literals
2594
2595 ReadOnlyCollection<GPALElement> elems = ElementHelper.FindWebElements(browser, browser.CurrentUOW, selector, out bool matchedAll, out matchedElements);
2596
2597 // FindWebElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
2598 if (true == selector.DeleteMe || null == elems || 0 == elems.Count)
2599 {
2600 PublishToEventHandler(browser, selector, elems?.ToList());
2601 continue;
2602 }
2603
2604 if (null != matchedElements)
2605 elems = new ReadOnlyCollection<GPALElement>(matchedElements);
2606
2607 foreach (GPALElement elem in elems)
2608 Hide(browser, elem);
2609 }
2610 }
2617 public static void Hide(Browser browser, GPALElement element)
2618 {
2619 if (true == browser.UseOttoMagic)
2620 browser.MagicHelper.HideElement(element.Css);
2621 else if (true == browser.UsePuppeteer)
2622 browser.PuppeteerClient.HideElement(element.ElementBackendNodeId.ToString());
2623 else
2624 BrowserHelper.ExecuteJavaScript(browser, "arguments[0].style.display = 'none';", element.WebElement);
2625
2626 }
2633 public static void SetAttribute(Browser browser, string attribute, string value)
2634 {
2635 List<GPALElement> matchedElements;
2636
2637 foreach (Selector selector in browser.CurrentUOW.WithSelectorList)
2638 {
2639 if (SelectorType.Selector != selector.SelectorType)
2640 continue; // we don't look up data literals
2641
2642 ReadOnlyCollection<GPALElement> elems = ElementHelper.FindWebElements(browser, browser.CurrentUOW, selector, out bool matchedAll, out matchedElements);
2643
2644 // FindWebElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
2645 if (true == selector.DeleteMe || null == elems || 0 == elems.Count)
2646 {
2647 PublishToEventHandler(browser, selector, elems?.ToList());
2648 continue;
2649 }
2650
2651 if (null != matchedElements)
2652 elems = new ReadOnlyCollection<GPALElement>(matchedElements);
2653
2654 foreach (GPALElement elem in elems)
2655 SetAttribute(browser, elem, attribute, value);
2656 }
2657 }
2667 public static void SetAttribute(Browser browser, GPALElement element, string attribute, string value)
2668 {
2669 if (true == browser.UseOttoMagic)
2670 browser.MagicHelper.SetAttribute(element.Css, attribute, value);
2671 else if (true == browser.UsePuppeteer)
2672 browser.PuppeteerClient.SetAttribute(element.ElementBackendNodeId.ToString()).WithAttribute(attribute).WithValue(value).Execute();
2673 else
2674 BrowserHelper.ExecuteJavaScript(browser, "arguments[0].setAttribute(arguments[1], arguments[2]);", element.WebElement, attribute, value);
2675 }
2682 public static void SetValueFromElement(Browser browser, Selector srcSelector)
2683 {
2684 List<GPALElement> matchedElements;
2685
2686 ReadOnlyCollection<GPALElement> srcElems = ElementHelper.FindWebElements(browser, browser.CurrentUOW, srcSelector, out bool srcMatchedAll, out List<GPALElement> srcMatchedElements);
2687
2688 if (null != srcMatchedElements)
2689 srcElems = new ReadOnlyCollection<GPALElement>(srcMatchedElements);
2690
2691 if (null == srcElems || 0 == srcElems.Count)
2692 return;
2693
2694 GPALElement srcElem = srcElems[0];
2695
2696 foreach (Selector selector in browser.CurrentUOW.WithSelectorList)
2697 {
2698 if (SelectorType.Selector != selector.SelectorType)
2699 continue; // we don't look up data literals
2700
2701 ReadOnlyCollection<GPALElement> elems = ElementHelper.FindWebElements(browser, browser.CurrentUOW, selector, out bool matchedAll, out matchedElements);
2702
2703 // FindWebElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
2704 if (true == selector.DeleteMe || null == elems || 0 == elems.Count)
2705 {
2706 PublishToEventHandler(browser, selector, elems?.ToList());
2707 continue;
2708 }
2709
2710 if (null != matchedElements)
2711 elems = new ReadOnlyCollection<GPALElement>(matchedElements);
2712
2713 foreach (GPALElement elem in elems)
2714 SetValueFromElement(browser, elem, srcElem);
2715 }
2716 }
2725 public static void SetValueFromElement(Browser browser, GPALElement element, GPALElement srcElement)
2726 {
2727 if (true == browser.UseOttoMagic)
2728 browser.MagicHelper.SetValueFromElement(srcElement.Css, element.Css);
2729 else if (true == browser.UsePuppeteer)
2730 browser.PuppeteerClient.WithElementId(srcElement.Css).SetValueFrom(element.Css).Execute();
2731 else
2732 BrowserHelper.ExecuteJavaScript(browser,
2733 "arguments[0].value = arguments[1].value; arguments[0].dispatchEvent(new Event('input', {bubbles: true})); arguments[0].dispatchEvent(new Event('change', {bubbles: true}));",
2734 element.WebElement, srcElement.WebElement);
2735 }
2754 public static void MoveTo(Browser browser)
2755 {
2756 List<GPALElement> matchedElements;
2757 UnitOfWork currentUOW = browser.CurrentUOW;
2758
2759 foreach (Selector selector in currentUOW.WithSelectorList)
2760 {
2761 if (SelectorType.Selector != selector.SelectorType)
2762 continue; // we don't look up data literals
2763
2764 ReadOnlyCollection<GPALElement> elems = ElementHelper.FindWebElements(browser, currentUOW, selector, out bool matchedAll, out matchedElements);
2765
2766 // FindWebElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
2767 if (true == selector.DeleteMe || null == elems || 0 == elems.Count)
2768 {
2769 PublishToEventHandler(browser, selector, elems?.ToList());
2770 continue;
2771 }
2772
2773 if (null != matchedElements)
2774 elems = new ReadOnlyCollection<GPALElement>(matchedElements);
2775
2776 int rowCount = 0;
2777
2778 foreach (GPALElement elem in elems)
2779 {
2780 if (null == elem)
2781 continue;
2782
2783 if (0 == elem.Size.Width && 0 == elem.Size.Height)
2784 {
2785 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Element has Zero Width and Height, skipping.", browser, GPALObjectType.Browser);
2786 continue;
2787 }
2788
2789 // an image match is a surrogate with coordinates and no DOM handle, so no engine can hover it
2790 // through the page - windowed it always goes to the hardware mouse, which is what image
2791 // matching was built around. selector level .WithHardware is honored here too, matching
2792 // what DragAndDrop already does.
2793 if (false == browser.BrowserSettings.UseHeadless
2794 && ("GPALElement" == elem.TagName
2795 || true == browser.BrowserSettings.UseHardware
2796 || true == GPAL.GPALSettings.UseHardware
2797 || InteractionType.Hardware == selector.InteractionType))
2798 {
2799 HardwareMoveTo(browser, elem, selector);
2800 }
2801 else if ("GPALElement" == elem.TagName)
2802 {
2803 // headless image match - no cursor to move and no handle to hover, so drive the engine's
2804 // own mouse at the coordinate instead
2805 MoveTo(browser, elem, selector);
2806 }
2807 else if (true == browser.UseOttoMagic || true == browser.UsePuppeteer)
2808 {
2809 MoveTo(elem, selector.OffsetX, selector.OffsetY, browser);
2810 }
2811 else if (Enums.InteractionType.JavaScript == selector.InteractionType || true == browser.BrowserSettings.UseJavaScript || true == browser.BrowserSettings.UseHeadless)
2812 {
2813 if (null != currentUOW.ShadowRoot)
2814 JavaScriptHover(browser, currentUOW.ShadowRoot);
2815 else
2816 JavaScriptHover(browser, elem);
2817 }
2818 else // selenium with a real cursor
2819 {
2820 MoveTo(elem, selector.OffsetX, selector.OffsetY, browser);
2821 }
2822
2823 if (++rowCount >= browser.CurrentUOW.WithAllThatMatch)
2824 break;
2825 }
2826 }
2827 }
2828
2838 private static void MoveTo(Browser browser, GPALElement element, Selector selector)
2839 {
2840 Point point = ToViewportPoint(browser, element, selector);
2841
2842 if (true == browser.UsePuppeteer)
2843 browser.PuppeteerClient.MoveTo(point).Execute();
2844 else if (true == browser.UseSelenium)
2845 new Actions(browser.BrowserDriver).MoveToLocation(point.X, point.Y).Perform();
2846 else
2847 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Cannot hover the image match for selector [{selector.Name}] - OttoMagic has no coordinate mouse move and there is no hardware mouse available.", browser, GPALObjectType.Browser);
2848 }
2849 // per-thread so concurrent browsers do not share a generator, and seeded from a Guid because the
2850 // parameterless Random seeds off a low resolution clock - constructed back to back it repeats itself
2851 [ThreadStatic] private static Random _jitter;
2852 private static Random Jitter => _jitter ?? (_jitter = new Random(Guid.NewGuid().GetHashCode()));
2861 private static Point ImageCenterOffset(Size size)
2862 {
2863 // never jitter so far that a tiny match is missed
2864 int jitterX = Math.Max(0, Math.Min(2, (size.Width / 2) - 1));
2865 int jitterY = Math.Max(0, Math.Min(2, (size.Height / 2) - 1));
2866
2867 return new Point((size.Width / 2) + Jitter.Next(-jitterX, jitterX + 1),
2868 (size.Height / 2) + Jitter.Next(-jitterY, jitterY + 1));
2869 }
2879 private static Point ToViewportPoint(Browser browser, GPALElement element, Selector selector)
2880 {
2881 // both zero means nothing was asked for, same rule the hardware path uses
2882 Point aim = (0 == selector.OffsetX && 0 == selector.OffsetY)
2883 ? ImageCenterOffset(element.Size)
2884 : new Point(selector.OffsetX, selector.OffsetY);
2885
2886 int pointX = element.Location.X + aim.X;
2887 int pointY = element.Location.Y + aim.Y;
2888
2889 if (CoordinateSpace.Screen == element.CoordinateSpace)
2890 {
2891 Rectangle content = GetWindowRectangle(browser);
2892 pointX -= content.X;
2893 pointY -= content.Y;
2894 }
2895
2896 return new Point(pointX, pointY);
2897 }
2906 private static void Click(Browser browser, Point point, ClickType clickType, ModifierKeys modifierKeys)
2907 {
2908 if (true == browser.UsePuppeteer)
2909 {
2910 switch (clickType)
2911 {
2912 case ClickType.LeftClick:
2913 browser.PuppeteerClient.LeftClick(point).WithModifiers(modifierKeys).Execute();
2914 break;
2915 case ClickType.LeftDoubleClick:
2916 browser.PuppeteerClient.LeftDoubleClick(point).WithModifiers(modifierKeys).Execute();
2917 break;
2918 case ClickType.MiddleClick:
2919 browser.PuppeteerClient.MiddleClick(point).WithModifiers(modifierKeys).Execute();
2920 break;
2921 case ClickType.RightClick:
2922 browser.PuppeteerClient.RightClick(point).WithModifiers(modifierKeys).Execute();
2923 break;
2924 }
2925 }
2926 else if (true == browser.UseSelenium)
2927 {
2928 Actions action = new Actions(browser.BrowserDriver).MoveToLocation(point.X, point.Y);
2929
2930 if (modifierKeys.HasFlag(ModifierKeys.Alt))
2931 action.KeyDown(OpenQA.Selenium.Keys.LeftAlt).KeyDown(OpenQA.Selenium.Keys.Alt);
2932 if (modifierKeys.HasFlag(ModifierKeys.Control))
2933 action.KeyDown(OpenQA.Selenium.Keys.LeftControl).KeyDown(OpenQA.Selenium.Keys.Control);
2934 if (modifierKeys.HasFlag(ModifierKeys.Shift))
2935 action.KeyDown(OpenQA.Selenium.Keys.LeftShift).KeyDown(OpenQA.Selenium.Keys.Shift);
2936 if (modifierKeys.HasFlag(ModifierKeys.Windows))
2937 action.KeyDown(OpenQA.Selenium.Keys.Meta);
2938
2939 switch (clickType)
2940 {
2941 case ClickType.LeftClick:
2942 action.Click();
2943 break;
2944 case ClickType.LeftDoubleClick:
2945 action.DoubleClick();
2946 break;
2947 case ClickType.RightClick:
2948 action.ContextClick();
2949 break;
2950 default:
2951 // selenium's Actions has no middle click to bind to
2952 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"[{clickType}] on an image match by coordinate is not supported by Selenium, performing a left click.", browser, GPALObjectType.Browser);
2953 action.Click();
2954 break;
2955 }
2956
2957 if (modifierKeys.HasFlag(ModifierKeys.Alt))
2958 action.KeyUp(OpenQA.Selenium.Keys.LeftAlt).KeyUp(OpenQA.Selenium.Keys.Alt);
2959 if (modifierKeys.HasFlag(ModifierKeys.Control))
2960 action.KeyUp(OpenQA.Selenium.Keys.LeftControl).KeyUp(OpenQA.Selenium.Keys.Control);
2961 if (modifierKeys.HasFlag(ModifierKeys.Shift))
2962 action.KeyUp(OpenQA.Selenium.Keys.LeftShift).KeyUp(OpenQA.Selenium.Keys.Shift);
2963 if (modifierKeys.HasFlag(ModifierKeys.Windows))
2964 action.KeyUp(OpenQA.Selenium.Keys.Meta);
2965
2966 ModifierKeys unsupported = modifierKeys & (ModifierKeys.Application | ModifierKeys.ScrollLock);
2967 if (ModifierKeys.NONE != unsupported)
2968 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"[{clickType}]: [{Browser.GetModifierKeys(unsupported)}] is not supported via Selenium/WebDriver Actions and was ignored.", browser, GPALObjectType.Browser);
2969
2970 action.Build().Perform();
2971 }
2972 else
2973 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Cannot click the image match by coordinate - OttoMagic has no coordinate click and there is no hardware mouse available.", browser, GPALObjectType.Browser);
2974 }
2984 private static void FillInFrom(Browser browser, Point point, string text, WriteMode writeMode)
2985 {
2986 // there is no handle to set a value on, so focus it the way a person would and type
2987 Click(browser, point, ClickType.LeftClick, ModifierKeys.NONE);
2988
2989 if (true == browser.UsePuppeteer)
2990 {
2991 if (WriteMode.Overwrite == writeMode)
2992 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "[Overwrite] cannot clear an image match by coordinate, the text will be added to whatever is already there.", browser, GPALObjectType.Browser);
2993
2994 browser.PuppeteerClient.SendString(text).Execute();
2995 }
2996 else if (true == browser.UseSelenium)
2997 {
2998 Actions actions = new Actions(browser.BrowserDriver);
2999
3000 if (WriteMode.Overwrite == writeMode)
3001 actions.KeyDown(Keys.Control).SendKeys("a").KeyUp(Keys.Control);
3002
3003 actions.SendKeys(text).Perform();
3004 }
3005 else
3006 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Cannot fill in the image match by coordinate - OttoMagic has no coordinate click to focus with and there is no hardware keyboard available.", browser, GPALObjectType.Browser);
3007 }
3016 private static void MoveTo(GPALElement element, int offsetX, int offsetY, Browser browser)
3017 {
3018 try
3019 {
3020 if (true == browser.UseOttoMagic)
3021 {
3022 browser.MagicHelper.MoveTo(element.Css);
3023 }
3024 else if (true == browser.UsePuppeteer)
3025 {
3026 browser.PuppeteerClient.MoveTo(element.ElementHandle).Execute();
3027 }
3028 else
3029 new Actions(browser.BrowserDriver).MoveToElement(element.WebElement, offsetX, offsetY).Perform();
3030 }
3031 catch (GPALException)
3032 {
3033 throw;
3034 }
3035 catch (Exception ex)
3036 {
3037 if (false == GPAL.NoFallbackRecoveryActions)
3038 {
3039 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Selenium threw an excaption during move to Selector [{browser.CurrentUOW.CurrentSelector?.Name}], falling back to javascript.", browser, GPALObjectType.Browser, ex);
3040 ElementHelper.ScrollIntoView(browser, element);
3041 }
3042 else
3043 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Selenium threw an excaption during move to Selector [{browser.CurrentUOW.CurrentSelector?.Name}], continuing.", browser, GPALObjectType.Browser, ex);
3044
3045 }
3046
3047 }
3056 private static void HardwareFocus(Browser browser, GPALElement webElement, int OffsetX, int OffsetY)
3057 {
3058 // focusing means putting the element where a real mouse can reach it, so the scroll belongs here rather
3059 // than in every caller. below the fold the element's screen position is a large positive Y that passes
3060 // the coordinate check in HardwareClick and lands on whatever is down there instead
3061 ScrollIntoView(browser, webElement, Enums.InteractionType.Hardware);
3062
3063 // IsElementEditable only counts the text-ish inputs, and the picker inputs take typing just as much.
3064 // without the click they never get focus, and the Ctrl+A that follows selects the whole document
3065 bool isPickerInput = valueOnlyInputTypes.Contains(webElement.Type?.ToLowerInvariant() ?? "");
3066
3067 if (true == ElementHelper.IsElementEditable(webElement) || true == isPickerInput)
3068 {
3069 // Safe / desired: click to focus (includes text inputs, textareas, contenteditable, etc.)
3070 HardwareClick(browser, webElement, ClickType.LeftClick, ModifierKeys.NONE, browser.CurrentUOW.CurrentSelector);
3071 }
3072 else
3073 {
3074 // Only move — avoid activating menus, toggles, navigation, submits, file pickers
3075 HardwareMoveTo(browser, webElement, browser.CurrentUOW.CurrentSelector);
3076 }
3077 }
3086 private static Rectangle HardwareMoveTo(Browser browser, dynamic element, Selector selector)
3087 {
3088 Rectangle content = new Rectangle(), iframeRect = new Rectangle(0, 0, 0, 0);
3089 bool isImageMatch = "GPALElement" == (string)element.TagName;
3090
3091 // where it was found is not where it is. hardware interaction scrolls the element into view before it
3092 // clicks, so by here the reading taken when the element was found describes a place it has left, and
3093 // an element that was below the fold still computes to a point off the bottom of the window. the
3094 // placement check then declines a click on an element that is sitting in plain view.
3095 // an image match is a verified screen position and has nothing to re-read
3096 if (false == isImageMatch)
3097 RefreshLocation(browser, (GPALElement)element);
3098
3099 Rectangle rect2 = new Rectangle(element.Location, element.Size);
3100 int offsetX = selector.OffsetX;
3101 int offsetY = selector.OffsetY;
3102
3103 // selenium reports IWebElement.Location in document coordinates, so it does not move when the page
3104 // scrolls - an element scrolled into view still reports where it sat at the top of the document, and
3105 // the mouse lands a full scroll offset away. the scroll the element was measured against rides along
3106 // on the element, so take it off to get where it actually sits in the viewport. an engine that reports
3107 // viewport coordinates sends no scroll, so nothing comes off those.
3108 Point scroll = Point.Empty;
3109 if (false == isImageMatch)
3110 {
3111 int pagex = 0;
3112 int pagey = 0;
3113
3114 if (true == browser.UseOttoMagic)
3115 {
3116 pagex = browser.MagicHelper.WindowPageOffsetX();
3117 pagey = browser.MagicHelper.WindowPageOffsetY();
3118 scroll = new Point(pagex, pagey);
3119 }
3120 else if (true == browser.UsePuppeteer)
3121 {
3122 pagex = browser.PuppeteerClient.WindowPageOffsetX().Execute<int>();
3123 pagey = browser.PuppeteerClient.WindowPageOffsetY().Execute<int>();
3124 }
3125 else
3126 {
3127 scroll = GetPageOffset(browser);
3128 }
3129 }
3130
3131 // find the absolute screen position of the webelement
3132 // if it's a posuedo GPALElement, then it is an image match and those coords are already absolute
3133 // iframe or shadowdom nesting, get parent coords
3134 if (0 < browser.CurrentUOW.ContextPath?.Count)
3135 {
3136 foreach (UnitOfWork.WebElementWithType contextType in browser.CurrentUOW.ContextPath)
3137 {
3138 if (ElementType.IFrame == contextType.elementType)
3139 {
3140 ClientRectangle tmpRect = contextType.gPalElement.BoundingRect;
3141 iframeRect.X += (int)contextType.gPalElement.Location.X;
3142 iframeRect.Y += (int)contextType.gPalElement.Location.Y;
3143 }
3144 }
3145 }
3146
3147 // if the click/hover offset is not specified, pick a point inside the element
3148 if (0 == offsetX && 0 == offsetY)
3149 {
3150 if ("GPALElement" == (string)element.TagName)
3151 {
3152 // an image match points at one specific graphic, so aim at the middle of it
3153 Point center = ImageCenterOffset(rect2.Size);
3154 offsetX = center.X;
3155 offsetY = center.Y;
3156 }
3157 else
3158 {
3159 // the middle, give or take. a point anywhere in the element lands on padding, a border or a
3160 // child that swallows the click, and the middle is where every other engine and every person
3161 // aims. the deviation keeps the exact same pixel from being hit every time, and is small
3162 // enough on a narrow element that it cannot walk off the middle of it
3163 int spreadX = Math.Max(1, (int)rect2.Width / 10);
3164 int spreadY = Math.Max(1, (int)rect2.Height / 10);
3165
3166 offsetX = (int)rect2.Width / 2 + Jitter.Next(-spreadX, spreadX + 1);
3167 offsetY = (int)rect2.Height / 2 + Jitter.Next(-spreadY, spreadY + 1);
3168 }
3169 }
3170
3171 content = GetWindowRectangle(browser);
3172
3173 // an image-matched surrogate carries whichever origin its capture used. a screen capture is already
3174 // absolute and needs nothing; a visible-tab capture is viewport relative and has to be shifted by the
3175 // content area origin to become a screen coordinate. display scaling is assumed to be 100%.
3176 Point imageOrigin = Point.Empty;
3177
3178 if ("GPALElement" == (string)element.TagName && CoordinateSpace.Viewport == ((GPALElement)element).CoordinateSpace)
3179 imageOrigin = new Point(content.X, content.Y);
3180
3181 // === FINAL SCREEN COORDS
3182 int screenX = (int)(("GPALElement" == element.TagName)
3183 ? rect2.Left + offsetX + imageOrigin.X
3184 : rect2.Left + offsetX + iframeRect.X + content.X - scroll.X);
3185
3186 int screenY = (int)(("GPALElement" == element.TagName)
3187 ? rect2.Top + offsetY + imageOrigin.Y
3188 : rect2.Top + offsetY + iframeRect.Y + content.Y - scroll.Y);
3189
3190 // an element bigger than the window cannot be scrolled fully on screen, and its left edge - the point
3191 // computed above - sits outside it. a file input stretched across its button is the usual one. only
3192 // here is the point adjusted, and only into the element's own visible area, so it is still the element
3193 // being clicked. an ordinary element that is merely scrolled off gets scrolled to instead
3194 if ("GPALElement" != (string)element.TagName
3195 && true == IsBiggerThanViewport(new Size((int)rect2.Width, (int)rect2.Height), content)
3196 && false == content.Contains(screenX, screenY))
3197 {
3198 // from where the element starts, not from the point being clicked. screenX already has offsetX
3199 // added, so measuring the overlap from there describes a rectangle half an element away from the
3200 // one on the page, and the part it calls visible is not the element's
3201 Point origin = new Point(screenX - offsetX, screenY - offsetY);
3202 Rectangle onScreen = Rectangle.Intersect(new Rectangle(origin.X, origin.Y, (int)rect2.Width, (int)rect2.Height), content);
3203
3204 if (false == onScreen.IsEmpty)
3205 {
3206 // the middle of what is on screen is a guess, and being inside the element's rectangle is not
3207 // the same as being reachable: an oversized element is mostly not its styled control, and
3208 // something else is often on top. so ask the page who would receive each point and take the
3209 // first that answers with this element. the middle is tried first because it is the point we
3210 // wanted anyway, and on an ordinary element it answers yes and nothing else is tried
3211 bool verified = false;
3212 int tryY = onScreen.Y + onScreen.Height / 2; // vertically it fits, so the middle is the middle
3213 List<int> candidates = new List<int>();
3214
3215 foreach (int inset in hitTestInsets)
3216 if (inset < onScreen.Width)
3217 candidates.Add(onScreen.Right - inset);
3218
3219 foreach (float fraction in hitTestFractions)
3220 candidates.Add(onScreen.X + (int)(onScreen.Width * fraction));
3221
3222 foreach (int tryX in candidates)
3223 if (true == PointHitsElement(browser, (GPALElement)element, new Point(tryX, tryY)))
3224 {
3225 screenX = tryX;
3226 screenY = tryY;
3227 verified = true;
3228 break;
3229 }
3230
3231 if (false == verified)
3232 {
3233 // nothing in the element's visible region belongs to it, so this is a click that is going
3234 // to miss whatever we do. the middle is as good a guess as any and saying so beats a
3235 // silent miss
3236 screenX = onScreen.X + onScreen.Width / 2;
3237 screenY = onScreen.Y + onScreen.Height / 2;
3238 }
3239
3240 // said as an offset from the element, which is the form WithOffset takes, so a workflow that
3241 // finds this point works can keep it and stop needing the correction. it is the click that
3242 // reports it, not this - working out a point is not clicking one
3243 _lastClampedPoint = $"at [WithOffset({screenX - origin.X}, {screenY - origin.Y})]{(true == verified ? "" : ", which nothing on this element answered to")}, for window [{_lastOuterWindow.X}, {_lastOuterWindow.Y}, {_lastOuterWindow.Width}, {_lastOuterWindow.Height}], because the element measures [{(int)rect2.Width}x{(int)rect2.Height}], larger than the window";
3244 }
3245 }
3246
3247 // === MOVE MOUSE ===
3248 if (true == GPAL.SimulateMouse || true == selector.SimulateMouse)
3249 HardwareHelper.MoveMouse(screenX, screenY, 10, 10);
3250 else
3251 HardwareHelper.MoveMouse(screenX, screenY);
3252
3253 return new Rectangle(screenX, screenY, (int)rect2.Width, (int)rect2.Height);
3254 }
3262 internal static Point GetPageOffset(Browser browser)
3263 {
3264 Point pageOffset = Point.Empty;
3265
3266 if (true == browser.UseOttoMagic)
3267 pageOffset = new Point(browser.MagicHelper.WindowPageOffsetX(), browser.MagicHelper.WindowPageOffsetY());
3268 else if (true == browser.UsePuppeteer)
3269 pageOffset = new Point(browser.PuppeteerClient.WindowPageOffsetX().Execute<int>(), browser.PuppeteerClient.WindowPageOffsetY().Execute<int>());
3270 else if (BrowserHelper.ExecuteJavaScriptObj("return [window.top.pageXOffset, window.top.pageYOffset]", browser) is IList<object> offsets && 2 <= offsets.Count)
3271 pageOffset = new Point(Convert.ToInt32(offsets[0]), Convert.ToInt32(offsets[1]));
3272
3273 return pageOffset;
3274 }
3284 private static bool UsesScreenCoordinates(Browser browser, Enums.InteractionType? interactionType)
3285 {
3286 Enums.InteractionType? useType = interactionType ?? browser.CurrentUOW?.CurrentSelector?.InteractionType;
3287
3288 return true == browser.BrowserSettings.UseHardware
3289 || true == GPAL.GPALSettings.UseHardware
3290 || Enums.InteractionType.Hardware == useType;
3291 }
3300 internal static void ScrollIntoView(Browser browser, dynamic element, Enums.InteractionType? interactionType = null)
3301 {
3302 if ("GPALElement".Equals(((GPALElement)element).TagName))
3303 return; // image surrogate, it's already on-screen if matched
3304
3305 // ottomagic works entirely through javascript against the css, so where the element sits on screen
3306 // makes no difference to it. only the hw engines and a hardware selector click a real coordinate
3307 if (true == browser.UseOttoMagic && false == UsesScreenCoordinates(browser, interactionType))
3308 return;
3309
3310 if (true == browser.UseOttoMagic)
3311 browser.MagicHelper.ScrollIntoView(((GPALElement)element).Css);
3312 else if (true == browser.UsePuppeteer)
3313 browser.PuppeteerClient.ScrollIntoView((GPALElement)element).Execute();
3314 else if (null != element) // NOTE: CAVEAT: BUG: this is hiding a bug in selenium getgrid scrollintoview of the element... (??? is this relevant any more?)
3315 {
3316 string debounceScript = @"
3317 // Best JS replacement for scrollIntoViewIfNeeded - do not scroll to if it's already on-screen
3318 function scrollIntoViewIfNeeded(el) {
3319 return new Promise(resolve => {
3320 const io = new IntersectionObserver(([entry]) => {
3321 io.disconnect();
3322 if (entry.intersectionRatio < 1) {
3323 el.scrollIntoView({ behavior: 'instant', block: 'center', inline: 'center' });
3324 }
3325 resolve();
3326 }, { threshold: 1 });
3327 io.observe(el);
3328 });
3329 }
3330
3331 scrollIntoViewIfNeeded(arguments[0]);
3332 ";
3333
3334 BrowserHelper.ExecuteJavaScript(browser, debounceScript, typeof(GPALElement) == element.GetType() ? ((GPALElement)element).WebElement : element);
3335 //BrowserHelper.ExecuteJavaScript(browser, "arguments[0].scrollIntoView()", element.WebElement);
3336 }
3337 Thread.Sleep(250); // give the dom time to catch up before we start asking about it, we apparently can ask for page offset faster than it ha it...
3338
3339 // every scroll above moves the document, which cannot move a fixed or sticky element, so any of them
3340 // can return having changed nothing while the element is still off-screen. page the window to it
3341 ScrollUntilVisible(browser, (GPALElement)element);
3342 }
3350 public static void LeftClick(Browser browser, ModifierKeys modifierKeys = ModifierKeys.NONE, ClickType clickType = ClickType.LeftClick)
3351 {
3352 List<GPALElement> matchedElements;
3353 List<GPALElement> clickableElements;
3354
3355 foreach (Selector selector in browser.CurrentUOW.WithSelectorList)
3356 {
3357 if (SelectorType.Selector != selector.SelectorType)
3358 continue; // we don't look up data literals
3359
3360 ElementHelper.FindWebElements(browser, browser.CurrentUOW, selector, out bool matchedAll, out matchedElements);
3361
3362 // FindWebElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
3363 if (true == selector.DeleteMe || null == matchedElements)
3364 {
3365 PublishToEventHandler(browser, selector, matchedElements);
3366 continue;
3367 }
3368
3369 // NOTE: _ALWAYS_ used matched elements, it will be the same as found elements if no match criteris
3370 // if match critieria, we want to use it cause it will be matched elems or none
3371
3372 if (1 < matchedElements.Count)
3373 {
3374 clickableElements = new List<GPALElement>();
3375 foreach (GPALElement elem in matchedElements)
3376 if (true == elem.IsClickable(false)) // no output from isClickable
3377 clickableElements.Add(elem);
3378
3379 if (0 == clickableElements.Count)
3380 clickableElements = matchedElements;
3381 }
3382 else
3383 clickableElements = matchedElements;
3384
3385 int rowCount = 0;
3386
3387 foreach (GPALElement elem in clickableElements)
3388 {
3389 if (null != elem)
3390 {
3391 if (true == elem.TagName.ToLower().Equals("option"))
3392 SelectClick(browser, selector, elem, SelectClickType.LeftClick, modifierKeys);
3393 else
3394 Click(browser, selector, elem, clickType, modifierKeys);
3395
3396 if (++rowCount >= browser.CurrentUOW.WithAllThatMatch)
3397 break;
3398 }
3399 }
3400
3401 //Thread.Sleep(500);
3402 }
3403 }
3408 public static void MiddleClick(Browser browser)
3409 {
3410 LeftClick(browser, ModifierKeys.NONE, ClickType.MiddleClick);
3411 }
3412
3420 public static void ScrollElement(Browser browser, int amount, ScrollTypes scrollTypes)
3421 {
3422 List<GPALElement> matchedElements;
3423 bool matchedAll = false;
3424
3425 foreach (Selector selector in browser.CurrentUOW.WithSelectorList)
3426 {
3427 if (SelectorType.Selector != selector.SelectorType)
3428 continue; // we don't look up data literals
3429
3430 ElementHelper.FindWebElements(browser, browser.CurrentUOW, selector, out matchedAll, out matchedElements);
3431
3432 if (null != matchedElements)
3433 {
3434 int rowCount = 0;
3435
3436 foreach (GPALElement elem in matchedElements)
3437 {
3438 if (null != elem)
3439 {
3440 int hAmount = ScrollTypes.Horizontal == scrollTypes ? amount : 0;
3441 int vAmount = ScrollTypes.Vertical == scrollTypes ? amount : 0;
3442
3443 if (true == browser.UseOttoMagic)
3444 browser.MagicHelper.ScrollElement(elem.Css, hAmount, vAmount);
3445 else if (true == browser.UsePuppeteer)
3446 browser.PuppeteerClient.ScrollElement(elem.Css).WithHPixels(hAmount).WithVPixels(vAmount).Execute();
3447 else if (true == browser.UseSelenium)
3448 BrowserHelper.ExecuteJavaScript(browser, $"arguments[0].scrollBy(left: {hAmount}, top: {vAmount}, behavior: 'instant')", elem.WebElement);
3449
3450 if (++rowCount >= browser.CurrentUOW.WithAllThatMatch)
3451 break;
3452 }
3453 }
3454 }
3455 }
3456 }
3464 public static bool GenericClick(Browser browser, ClickType clickType)
3465 {
3466 List<GPALElement> matchedElements;
3467 bool matchedAll = false;
3468 bool clickPerformed = false;
3469
3470 foreach (Selector selector in browser.CurrentUOW.WithSelectorList)
3471 {
3472 if (SelectorType.Selector != selector.SelectorType)
3473 continue; // we don't look up data literals
3474
3475 ElementHelper.FindWebElements(browser, browser.CurrentUOW, selector, out matchedAll, out matchedElements);
3476
3477 if (null != matchedElements)
3478 {
3479 int rowCount = 0;
3480 foreach (GPALElement elem in matchedElements)
3481 {
3482 if (null != elem)
3483 {
3484 if (true == elem.TagName.ToLower().Equals("option"))
3485 SelectClick(browser, selector, elem, SelectClickType.LeftClick, ModifierKeys.NONE);
3486 else
3487 Click(browser, selector, elem, clickType, ModifierKeys.NONE);
3488
3489 clickPerformed = true;
3490
3491 if (++rowCount >= browser.CurrentUOW.WithAllThatMatch)
3492 break;
3493 }
3494 }
3495 }
3496 }
3497 return clickPerformed;
3498 }
3506 public static bool GenericClick(Browser browser, SelectClickType selectClickType)
3507 {
3508 List<GPALElement> matchedElements;
3509 bool matchedAll = false;
3510 bool clickPerformed = false;
3511
3512 foreach (Selector selector in browser.CurrentUOW.WithSelectorList)
3513 {
3514 if (SelectorType.Selector != selector.SelectorType)
3515 continue; // we don't look up data literals
3516
3517 ElementHelper.FindWebElements(browser, browser.CurrentUOW, selector, out matchedAll, out matchedElements);
3518
3519 if (null != matchedElements)
3520 {
3521 int rowCount = 0;
3522 foreach (GPALElement elem in matchedElements)
3523 {
3524 if (null != elem)
3525 {
3526 if (true == elem.TagName.ToLower().Equals("option"))
3527 SelectClick(browser, selector, elem, selectClickType, ModifierKeys.NONE);
3528 else
3529 Click(browser, selector, elem, ClickType.LeftClick, ModifierKeys.NONE);
3530
3531 clickPerformed = true;
3532
3533 if (++rowCount >= browser.CurrentUOW.WithAllThatMatch)
3534 break;
3535 }
3536 }
3537 }
3538 }
3539 return clickPerformed;
3540 }
3541
3542 // Click type note
3543 // NOTE: Special types for Select menus: RandomSelect will pick a random menu item, SequentialSelect will walk the list (w/ & w/0 wrapping)
3544 // written by grok with updates from mbv
3545 internal static class SelectClickHelper
3546 {
3547 // Static dictionary to track the current index for SequentialSelect and SequentialSelectWithWrap
3548 internal static readonly Dictionary<string, int> _sequentialSelectIndices = new Dictionary<string, int>();
3549 // Track the last parentNode identifier
3550 internal static string _lastParentNodeId = null;
3551
3559 internal static string GetElementAttributeHashCsharp(GPALElement element, Browser browser)
3560 {
3561 try
3562 {
3563 // Get all attributes from the Attributes dictionary
3564 var attrList = new List<string>();
3565
3566 foreach (var attrName in element.Attributes.Keys)
3567 {
3568 // Use GetAttribute to retrieve the actual value
3569 string value = element.GetAttribute(attrName);
3570 if (!string.IsNullOrEmpty(value))
3571 {
3572 attrList.Add($"{attrName}={value}");
3573 }
3574 }
3575
3576 // Sort attributes for consistent hashing
3577 attrList.Sort();
3578 string attrString = string.Join("|", attrList);
3579
3580 // Compute hash (same algorithm as JavaScript)
3581 int hash = 0;
3582 foreach (char c in attrString)
3583 {
3584 hash = ((hash << 5) - hash + c) | 0; // Matches JavaScript: hash = ((hash << 5) - hash + charCode) | 0
3585 }
3586
3587 return hash.ToString();
3588 }
3589 catch
3590 {
3591 return element.GetHashCode().ToString(); // Fallback to CitrixHashCode
3592 }
3593 }
3594
3602 internal static string GetElementAttributeHashJavaScript(GPALElement element, Browser browser)
3603 {
3604 try
3605 {
3606 if (false == browser.UseOttoMagic)
3607 {
3608 string jsScript = @"
3609 let attrs = arguments[0].attributes;
3610 let attrList = [];
3611 for (let attr of attrs) {
3612 attrList.push(attr.name + '=' + attr.value);
3613 }
3614 attrList.sort(); // Sort attributes for consistent hashing
3615 let str = attrList.join('|');
3616 let hash = 0;
3617 for (let i = 0; i < str.length; i++) {
3618 hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
3619 }
3620 return hash.toString();
3621 ";
3622 return (string)BrowserHelper.ExecuteJavaScript(browser, jsScript, element.WebElement) ?? element.GetHashCode().ToString();
3623 } else
3624 {
3625 return browser.OttoMagicClient.GetElementAttributeHash(element.Css).Execute().Replace("\"","");
3626 }
3627 }
3628 catch
3629 {
3630 return element.GetHashCode().ToString(); // Fallback to GetHashCode
3631 }
3632 }
3633 }
3644
3645 public static void SelectClick(Browser browser, Selector selector, GPALElement webElement, SelectClickType clickType, ModifierKeys modifierKeys)
3646 {
3647 // refactored by grok to add random/sequential selects
3648 dynamic result = null; // GPALElement or WebElement
3649 dynamic parentElem = null; // GPALElement or WebElement
3650 SelectElement elem = null;
3651 string selectedValue = null;
3652 dynamic options = null;
3653 string elementId = null;
3654
3655 // NOTE: since we have select menu options like select a random entry or select next or select previous, we track the select menu internally and track the currently selected item
3656 // in theory this allows us to track multiple select menus but may be overkill
3657 if (true == browser.UseOttoMagic)
3658 result = browser.MagicHelper.GetParentNode(webElement.Css);
3659 else if (true == browser.UsePuppeteer)
3660 result = browser.PuppeteerClient.GetParentNode(webElement.ElementHandle).Execute<GPALElement>();
3661 else // selenium
3662 result = BrowserHelper.ExecuteJavaScriptObj("return arguments[0].parentNode;", browser, webElement.WebElement);
3663
3664 if (result != null)
3665 {
3666 if (true == browser.UseSelenium)
3667 {
3668 parentElem = result;
3669 elementId = parentElem.GetAttribute("id");
3670 }
3671 else
3672 {
3673 parentElem = (GPALElement)result;
3674 parentElem.Browser = browser;
3675 elementId = parentElem.GetAttribute("id");
3676 }
3677
3678 try
3679 {
3680 ScrollIntoView(browser, parentElem);
3681
3682 // Get a unique identifier based on attributes
3683 if (true == browser.UseSelenium)
3684 {
3685 elem = new SelectElement(parentElem);
3686
3687 // Get all options in the select element
3688 if (SelectClickType.LeftClick != clickType)
3689 {
3690 options = elem.Options;
3691 if (options.Count == 0)
3692 {
3693 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No options found in select element.", webElement, GPALObjectType.Other);
3694 return;
3695 }
3696 }
3697
3698 // Use C# attribute iteration for non-JavaScript interaction types
3699 if (string.IsNullOrEmpty(elementId))
3700 {
3701 //if (selector.InteractionType != Enums.InteractionType.JavaScript)
3702 // elementId = SelectClickHelper.GetElementAttributeHashCsharp(parentElem, browser);
3703 //else
3704 elementId = SelectClickHelper.GetElementAttributeHashJavaScript(parentElem, browser);
3705 }
3706 }
3707 else
3708 {
3709 if (SelectClickType.LeftClick != clickType)
3710 {
3711 Selector optionsSelector = GPAL.CssSelector($"{parentElem.Css} > option").WithSelectorName("options").ToGPALObject();
3712
3713 options = FindWebElementsByCss(browser, optionsSelector.SelectorPaths[0], optionsSelector); // CAVEAT: hardcoded value, but it's our selector
3714
3715 if (0 == options.Count)
3716 {
3717 Click(browser, selector, webElement, ClickType.LeftClick, modifierKeys);
3718 return;
3719 }
3720 }
3721
3722 if (string.IsNullOrEmpty(elementId))
3723 {
3724 if (true == browser.UseOttoMagic)
3725 elementId = browser.MagicHelper.GetElementAttributeHash(parentElem.Css);
3726 else if (true == browser.UsePuppeteer)
3727 elementId = browser.PuppeteerClient.GetElementAttributeHash(parentElem.Css).Execute();
3728 }
3729 }
3730
3731 // Check if we're dealing with a new select element
3732
3733 if (SelectClickHelper._lastParentNodeId != elementId &&
3734 (clickType == SelectClickType.SelectNext || clickType == SelectClickType.SelectNextWithWrap))
3735 {
3736 SelectClickHelper._sequentialSelectIndices[elementId] = -1; // Reset index
3737 }
3738
3739 SelectClickHelper._lastParentNodeId = elementId;
3740
3741 if (false == browser.BrowserSettings.UseHeadless && (true == browser.BrowserSettings.UseHardware || true == GPAL.GPALSettings.UseHardware))
3742 {
3743 switch (clickType)
3744 {
3745 case SelectClickType.SelectRandom:
3746 Random rand = new Random();
3747 int randomIndex = rand.Next(0, options.Count);
3748 // Store the new index and perform the selection
3749 SelectClickHelper._sequentialSelectIndices[elementId] = randomIndex;
3750 HardwareClick(browser, parentElem, ClickType.LeftClick, modifierKeys, browser.CurrentUOW.CurrentSelector);
3751 HardwareHelper.SendKey(GPAL.VK_HOME);
3752 for (int i = 0; i < randomIndex; i++)
3753 {
3754 Thread.Sleep(50);
3755 HardwareHelper.SendKey(GPAL.VK_DOWN);
3756 }
3757 HardwareHelper.SendKey(GPAL.VK_RETURN);
3758 break;
3759
3760 case SelectClickType.SelectNext:
3761 case SelectClickType.SelectNextWithWrap:
3762 case SelectClickType.SelectPrevious:
3763 case SelectClickType.SelectPreviousWithWrap:
3764
3765 // +1 = move down (next), -1 = move up (previous)
3766 int incOrDec = (clickType == SelectClickType.SelectPrevious ||
3767 clickType == SelectClickType.SelectPreviousWithWrap)
3768 ? -1 : 1;
3769
3770 bool shouldWrap = (clickType == SelectClickType.SelectNextWithWrap ||
3771 clickType == SelectClickType.SelectPreviousWithWrap);
3772
3773 // Initialize to -1 on first use -> first Next -> index 0, first Previous -> last item
3774 if (!SelectClickHelper._sequentialSelectIndices.TryGetValue(elementId, out int currentIndex))
3775 {
3776 currentIndex = -1;
3777 SelectClickHelper._sequentialSelectIndices[elementId] = currentIndex;
3778 }
3779
3780 int nextIndex;
3781
3782 if (shouldWrap)
3783 {
3784 // Proper wrapping that works when going backwards from 0
3785 nextIndex = (currentIndex + incOrDec % options.Count + options.Count) % options.Count;
3786 }
3787 else
3788 {
3789 // No wrap -> clamp to bounds
3790 nextIndex = currentIndex + incOrDec;
3791 if (nextIndex < 0)
3792 nextIndex = 0;
3793 else if (nextIndex >= options.Count)
3794 nextIndex = options.Count - 1;
3795 }
3796
3797 // === Hardware emulation starts here ===
3798 HardwareClick(browser, parentElem, ClickType.LeftClick, modifierKeys,
3799 browser.CurrentUOW.CurrentSelector);
3800
3801 // Always start from the top (HOME) — this matches real dropdown behavior
3802 HardwareHelper.SendKey(GPAL.VK_HOME);
3803 Thread.Sleep(50); // small delay after HOME
3804
3805 if (incOrDec > 0)
3806 {
3807 // Going forward -> just press DOWN `nextIndex` times
3808 for (int i = 0; i < nextIndex; i++)
3809 {
3810 Thread.Sleep(50);
3811 HardwareHelper.SendKey(GPAL.VK_DOWN);
3812 }
3813 }
3814 else
3815 {
3816 // Going backward -> go to end first, then go up the required steps
3817 // This is the most reliable way when using pure keyboard
3818 HardwareHelper.SendKey(GPAL.VK_END);
3819 Thread.Sleep(50);
3820
3821 int stepsUp = options.Count - 1 - nextIndex; // how many UP arrows needed from bottom
3822 for (int i = 0; i < stepsUp; i++)
3823 {
3824 Thread.Sleep(50);
3825 HardwareHelper.SendKey(GPAL.VK_UP);
3826 }
3827 }
3828
3829 // Confirm selection
3830 HardwareHelper.SendKey(GPAL.VK_RETURN);
3831
3832 // Store the new index for next time
3833 SelectClickHelper._sequentialSelectIndices[elementId] = nextIndex;
3834
3835 break;
3836
3837 default:
3838 HardwareClick(browser, parentElem, ClickType.LeftClick, modifierKeys, browser.CurrentUOW.CurrentSelector);
3839
3840 // ReadOnlyCollection<GPALElement> optionElems = ElementHelper.FindWebElements(browser, browser.CurrentUOW, selector, out bool matchedAll, out List<GPALElement> matchedElements);
3841 int index = Int32.Parse(webElement.GetAttribute("index")); // optionElems[0].GetAttribute("index"));
3842 // Store the new index and perform the selection
3843 SelectClickHelper._sequentialSelectIndices[elementId] = index;
3844 HardwareHelper.SendKey(GPAL.VK_HOME);
3845 for (int cnt = 0; cnt < index; cnt++)
3846 {
3847 Thread.Sleep(50);
3848 HardwareHelper.SendKey(GPAL.VK_DOWN);
3849 }
3850 HardwareHelper.SendKey(GPAL.VK_RETURN);
3851 break;
3852 }
3853 }
3854 else if (true == browser.UseOttoMagic)
3855 {
3856 switch (clickType)
3857 {
3858 case SelectClickType.SelectRandom:
3859 Random rand = new Random();
3860 int randomIndex = rand.Next(0, options.Count);
3861 selectedValue = options[randomIndex].GetAttribute("value");
3862 // Store the new index and perform the selection
3863 SelectClickHelper._sequentialSelectIndices[elementId] = randomIndex;
3864 browser.MagicHelper.SelectClick(parentElem.Css, selectedValue);
3865 break;
3866
3867 case SelectClickType.SelectNext:
3868 case SelectClickType.SelectNextWithWrap:
3869 case SelectClickType.SelectPrevious:
3870 case SelectClickType.SelectPreviousWithWrap:
3871
3872 // +1 for next, -1 for previous
3873 int incOrDec = (clickType == SelectClickType.SelectPrevious ||
3874 clickType == SelectClickType.SelectPreviousWithWrap)
3875 ? -1 : 1;
3876
3877 // true if we should wrap around
3878 bool shouldWrap = (clickType == SelectClickType.SelectNextWithWrap ||
3879 clickType == SelectClickType.SelectPreviousWithWrap);
3880
3881 // Initialize to -1 on first use (so first "next" goes to 0, first "prev" goes to last)
3882 if (!SelectClickHelper._sequentialSelectIndices.TryGetValue(elementId, out int currentIndex))
3883 {
3884 currentIndex = -1;
3885 SelectClickHelper._sequentialSelectIndices[elementId] = currentIndex;
3886 }
3887
3888 int nextIndex;
3889
3890 if (shouldWrap)
3891 {
3892 // Safe modular arithmetic that works with negative numbers
3893 nextIndex = (currentIndex + incOrDec % options.Count + options.Count) % options.Count;
3894 }
3895 else
3896 {
3897 // Manual clamp: stay within [0, options.Count-1]
3898 nextIndex = currentIndex + incOrDec;
3899 if (nextIndex < 0)
3900 nextIndex = 0;
3901 else if (nextIndex >= options.Count)
3902 nextIndex = options.Count - 1;
3903 }
3904
3905 // Store the new index and perform the selection
3906 SelectClickHelper._sequentialSelectIndices[elementId] = nextIndex;
3907 selectedValue = options[nextIndex].GetAttribute("value");
3908 browser.MagicHelper.SelectClick(parentElem.Css, selectedValue);
3909
3910 break;
3911
3912 default:
3913 int index = Int32.Parse(webElement.GetAttribute("index"));
3914 // Store the new index and perform the selection
3915 SelectClickHelper._sequentialSelectIndices[elementId] = index;
3916 browser.MagicHelper.SelectClick(parentElem.Css, webElement.Value);
3917 break;
3918 }
3919 }
3920 else if (true == browser.UsePuppeteer)
3921 {
3922 switch (clickType)
3923 {
3924 case SelectClickType.SelectRandom:
3925 Random rand = new Random();
3926 int randomIndex = rand.Next(0, options.Count);
3927 selectedValue = options[randomIndex].GetAttribute("value");
3928 // Store the new index and perform the selection
3929 SelectClickHelper._sequentialSelectIndices[elementId] = randomIndex;
3930 // note, some weird c# late binding cannot find selectclick in the interface, thus the cast
3931 ((PuppeteerClient)browser.PuppeteerClient).SelectClick((dynamic)parentElem.ElementHandle).WithSelectValue(selectedValue).Execute();
3932 break;
3933
3934 case SelectClickType.SelectNext:
3935 case SelectClickType.SelectNextWithWrap:
3936 case SelectClickType.SelectPrevious:
3937 case SelectClickType.SelectPreviousWithWrap:
3938
3939 // +1 for next, -1 for previous
3940 int incOrDec = (clickType == SelectClickType.SelectPrevious ||
3941 clickType == SelectClickType.SelectPreviousWithWrap)
3942 ? -1 : 1;
3943
3944 // true if we should wrap around
3945 bool shouldWrap = (clickType == SelectClickType.SelectNextWithWrap ||
3946 clickType == SelectClickType.SelectPreviousWithWrap);
3947
3948 // Initialize to -1 on first use (so first "next" goes to 0, first "prev" goes to last)
3949 if (!SelectClickHelper._sequentialSelectIndices.TryGetValue(elementId, out int currentIndex))
3950 {
3951 currentIndex = -1;
3952 SelectClickHelper._sequentialSelectIndices[elementId] = currentIndex;
3953 }
3954
3955 int nextIndex;
3956
3957 if (shouldWrap)
3958 {
3959 // Safe modular arithmetic that works with negative numbers
3960 nextIndex = (currentIndex + incOrDec % options.Count + options.Count) % options.Count;
3961 }
3962 else
3963 {
3964 // Manual clamp: stay within [0, options.Count-1]
3965 nextIndex = currentIndex + incOrDec;
3966 if (nextIndex < 0)
3967 nextIndex = 0;
3968 else if (nextIndex >= options.Count)
3969 nextIndex = options.Count - 1;
3970 }
3971
3972 // Store the new index and perform the selection
3973 SelectClickHelper._sequentialSelectIndices[elementId] = nextIndex;
3974 selectedValue = options[nextIndex].GetAttribute("value");
3975 // note, some weird c# late binding cannot find selectclick in the interface, thus the cast
3976 ((PuppeteerClient)browser.PuppeteerClient).SelectClick((dynamic)parentElem.ElementHandle).WithSelectValue(selectedValue).Execute();
3977
3978 break;
3979
3980 default:
3981 int index = Int32.Parse(webElement.GetAttribute("index"));
3982 SelectClickHelper._sequentialSelectIndices[elementId] = index;
3983 // Store the new index and perform the selection
3984 // note, some weird c# late binding cannot find selectclick in the interface, thus the cast
3985 ((PuppeteerClient)browser.PuppeteerClient).SelectClick((dynamic)parentElem.ElementHandle).WithSelectIndex(index).Execute();
3986 break;
3987 }
3988 }
3989 else if (true == browser.UseSelenium)
3990 {
3991
3992 if (Enums.InteractionType.JavaScript == selector.InteractionType || browser.BrowserSettings.UseJavaScript) // only selenium can have javascript option
3993 {
3994 switch (clickType)
3995 {
3996 case SelectClickType.SelectRandom:
3997 Random rand = new Random();
3998 int randomIndex = rand.Next(0, options.Count);
3999 // Store the new index and perform the selection
4000 SelectClickHelper._sequentialSelectIndices[elementId] = randomIndex;
4001 selectedValue = options[randomIndex].GetAttribute("value");
4002 BrowserHelper.ExecuteJavaScript(browser, "arguments[0].value = arguments[1];", parentElem, selectedValue);
4003 break;
4004
4005 case SelectClickType.SelectNext:
4006 case SelectClickType.SelectNextWithWrap:
4007 case SelectClickType.SelectPrevious:
4008 case SelectClickType.SelectPreviousWithWrap:
4009
4010 // +1 for next, -1 for previous
4011 int incOrDec = (clickType == SelectClickType.SelectPrevious ||
4012 clickType == SelectClickType.SelectPreviousWithWrap)
4013 ? -1 : 1;
4014
4015 // true if we should wrap around
4016 bool shouldWrap = (clickType == SelectClickType.SelectNextWithWrap ||
4017 clickType == SelectClickType.SelectPreviousWithWrap);
4018
4019 // Initialize to -1 on first use (so first "next" goes to 0, first "prev" goes to last)
4020 if (!SelectClickHelper._sequentialSelectIndices.TryGetValue(elementId, out int currentIndex))
4021 {
4022 currentIndex = -1;
4023 SelectClickHelper._sequentialSelectIndices[elementId] = currentIndex;
4024 }
4025
4026 int nextIndex;
4027
4028 if (shouldWrap)
4029 {
4030 // Safe modular arithmetic that works with negative numbers
4031 nextIndex = (currentIndex + incOrDec % options.Count + options.Count) % options.Count;
4032 }
4033 else
4034 {
4035 // Manual clamp: stay within [0, options.Count-1]
4036 nextIndex = currentIndex + incOrDec;
4037 if (nextIndex < 0)
4038 nextIndex = 0;
4039 else if (nextIndex >= options.Count)
4040 nextIndex = options.Count - 1;
4041 }
4042
4043 // Store the new index and perform the selection
4044 SelectClickHelper._sequentialSelectIndices[elementId] = nextIndex;
4045 selectedValue = options[nextIndex].GetAttribute("value");
4046 BrowserHelper.ExecuteJavaScript(browser, "arguments[0].value = arguments[1];", parentElem, selectedValue);
4047 break;
4048
4049 default:
4050 int index = Int32.Parse(webElement.WebElement.GetAttribute("index")); // optionElems[0].GetAttribute("index"));
4051 // Store the new index and perform the selection
4052 SelectClickHelper._sequentialSelectIndices[elementId] = index;
4053 BrowserHelper.ExecuteJavaScript(browser, "arguments[0].value = arguments[1].value", parentElem, webElement.WebElement);
4054 break;
4055 }
4056 }
4057 else // use native selenium rather than javascript injection
4058 switch (clickType)
4059 {
4060 case SelectClickType.SelectRandom:
4061 Random rand = new Random();
4062 int randomIndex = rand.Next(0, options.Count);
4063 // Store the new index and perform the selection
4064 SelectClickHelper._sequentialSelectIndices[elementId] = randomIndex;
4065 elem.SelectByIndex(randomIndex);
4066 break;
4067
4068 case SelectClickType.SelectNext:
4069 case SelectClickType.SelectNextWithWrap:
4070 case SelectClickType.SelectPrevious:
4071 case SelectClickType.SelectPreviousWithWrap:
4072
4073 // Determine direction: +1 = next, -1 = previous
4074 int incOrDec = (clickType == SelectClickType.SelectPrevious ||
4075 clickType == SelectClickType.SelectPreviousWithWrap)
4076 ? -1 : 1;
4077
4078 // Determine if we should wrap around
4079 bool shouldWrap = (clickType == SelectClickType.SelectNextWithWrap ||
4080 clickType == SelectClickType.SelectPreviousWithWrap);
4081
4082 // Get current index, default to -1 (so first "next" -> 0, first "prev" -> last)
4083 if (!SelectClickHelper._sequentialSelectIndices.TryGetValue(elementId, out int currentIndex))
4084 {
4085 currentIndex = -1;
4086 SelectClickHelper._sequentialSelectIndices[elementId] = currentIndex;
4087 }
4088
4089 int nextIndex;
4090
4091 if (shouldWrap)
4092 {
4093 // Safe wrap-around that works with negative numbers
4094 nextIndex = (currentIndex + incOrDec % options.Count + options.Count) % options.Count;
4095 }
4096 else
4097 {
4098 // No wrap: clamp to valid range
4099 nextIndex = currentIndex + incOrDec;
4100 if (nextIndex < 0)
4101 nextIndex = 0;
4102 else if (nextIndex >= options.Count)
4103 nextIndex = options.Count - 1;
4104 }
4105
4106 // Perform the selection and store the new index
4107 elem.SelectByIndex(nextIndex);
4108 SelectClickHelper._sequentialSelectIndices[elementId] = nextIndex;
4109
4110 break;
4111
4112 default:
4113 int index = Int32.Parse(webElement.GetAttribute("index")); // optionElems[0].GetAttribute("index"));
4114 // Store the new index and perform the selection
4115 SelectClickHelper._sequentialSelectIndices[elementId] = index;
4116 elem.SelectByValue(webElement.GetAttribute("value"));
4117 break;
4118 }
4119 }
4120 }
4121 catch (GPALException)
4122 {
4123 throw;
4124 }
4125 catch (Exception ex)
4126 {
4127 if (!GPAL.NoFallbackRecoveryActions)
4128 {
4129 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to Select using [{selector.InteractionType}]. Falling back on JavaScript.", webElement, GPALObjectType.Other, ex);
4130 BrowserHelper.ExecuteJavaScript(browser, "arguments[0].value = arguments[1].value", true == browser.UseSelenium ? parentElem : ((GPALElement)parentElem).WebElement, webElement.WebElement);
4131 }
4132 else
4133 {
4134 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to Select using [{selector.InteractionType}].", webElement, GPALObjectType.Other, ex);
4135 }
4136 }
4137 selectedValue = null != options ? options[SelectClickHelper._sequentialSelectIndices[elementId]].GetAttribute("value") : webElement.GetAttribute("value");
4138 string selectedText = null != options ? options[SelectClickHelper._sequentialSelectIndices[elementId]].Text : webElement.Text;
4139 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{selector.InteractionType}] selected [{selectedText}] value [{selectedValue}] index [{SelectClickHelper._sequentialSelectIndices[elementId]}]");
4140 }
4141 else
4142 {
4143 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to click Parent Select for Option using [{selector.InteractionType}]. Clicking element [{selector.Name}].", webElement, GPALObjectType.Other);
4144 webElement.Click(modifierKeys);
4145 }
4146 }
4147
4158 public static void Click(Browser browser, Selector selector, GPALElement webElement, ClickType clickType, ModifierKeys modifierKeys)
4159 {
4160 bool dontPublish = false;
4161
4162 ScrollIntoView(browser, webElement, selector.InteractionType);
4163
4164
4165 //if (true == webElement.IsClickable()) // NOTE: no real utility to printing out if elemenent is clickable, we will try anyways, just slows us down a little
4166 {
4167 try
4168 {
4169 if (false == browser.BrowserSettings.UseHeadless
4170 && (Enums.InteractionType.Hardware == selector.InteractionType
4171 || "GPALElement" == webElement.TagName // image match - coordinates only, no handle any engine can click
4172 || true == browser.BrowserSettings.UseHardware || true == GPAL.GPALSettings.UseHardware)
4173 )
4174 {
4175 // scroll into view can and will change the coords. otto and puppeteer captured theirs off
4176 // getBoundingClientRect when the element was found, so those are stale now and have to be
4177 // requeried. selenium's are document coordinates that the scroll never moves, and the
4178 // hardware path re-measures them against the live viewport, so a requery buys it nothing.
4179 GPALElement clickMe = webElement;
4180
4181 if (false == browser.UseSelenium && false == string.IsNullOrEmpty(webElement.Css))
4182 {
4183 Selector cssSel = GPAL.Selector.WithCSS(webElement.Css).ToGPALObject();
4184 ReadOnlyCollection<GPALElement> elems = FindWebElementsByCss(browser, cssSel.SelectorPaths[0], cssSel);
4185
4186 // no match means the element is not reachable from the document root - a closed shadow
4187 // root's child, most often - so click what we were handed rather than throwing
4188 if (0 < elems?.Count)
4189 clickMe = elems[0];
4190 }
4191
4192 HardwareClick(browser, clickMe, clickType, modifierKeys, selector, true);
4193 dontPublish = true;
4194 }
4195 else if ("GPALElement" == webElement.TagName)
4196 {
4197 // headless image match - no handle for any engine, so click the coordinate directly
4198 Click(browser, ToViewportPoint(browser, webElement, selector), clickType, modifierKeys);
4199 }
4200 else if (true == browser.UseOttoMagic)
4201 {
4202 if (modifierKeys != ModifierKeys.NONE)
4203 browser.MagicHelper.PressModifierKey(modifierKeys);
4204
4205 switch (clickType)
4206 {
4207 case ClickType.LeftClick:
4208 browser.MagicHelper.LeftClick(webElement.Css);
4209 break;
4210 case ClickType.LeftDoubleClick:
4211 browser.MagicHelper.LeftDoubleClick(webElement.Css);
4212 break;
4213 case ClickType.MiddleClick:
4214 browser.MagicHelper.MiddleClick(webElement.Css);
4215 break;
4216 case ClickType.RightClick:
4217 browser.MagicHelper.RightClick(webElement.Css);
4218 break;
4219 }
4220
4221 if (modifierKeys != ModifierKeys.NONE)
4222 browser.MagicHelper.ReleaseModifierKey(modifierKeys);
4223 }
4224 else if (Enums.InteractionType.JavaScript == selector.InteractionType || true == browser.BrowserSettings.UseJavaScript) // selunium & puppeteer javascript override
4225 {
4226 if (false == JavaScriptClick(browser, webElement, clickType, modifierKeys) && false == GPAL.NoFallbackRecoveryActions)
4227 {
4228 if (true == browser.UseSelenium)
4229 {
4230 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"JavaScript click failed on selector [{selector.Name}], falling back to selenium click.", browser, GPALObjectType.Browser);
4231 SeleniumClick(browser, webElement, clickType, modifierKeys);
4232 }
4233 else
4234 {
4235 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"JavaScript click failed on selector [{selector.Name}], falling back to Puppeteer click.", browser, GPALObjectType.Browser);
4236 PuppeteerClick(browser, webElement, clickType, modifierKeys);
4237 dontPublish = true;
4238 }
4239 }
4240 }
4241 else if (true == browser.UsePuppeteer)
4242 {
4243 PuppeteerClick(browser, webElement, clickType, modifierKeys);
4244 dontPublish = true;
4245 }
4246 else
4247 SeleniumClick(browser, webElement, clickType, modifierKeys);
4248
4249 if (false == dontPublish)
4250 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Element [{webElement.TagName}] for selector [{selector.Name}] [{selector.InteractionType.ToString()}][{clickType.ToString()}] clicked with modifiers [{GetEnumNames(modifierKeys)}].", browser, GPALObjectType.Browser);
4251
4252 }
4253 catch (GPALException)
4254 {
4255 throw;
4256 }
4257 catch (Exception ex)
4258 {
4259 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to [{clickType.ToString()}] click element [{webElement.TagName}] for selector [{selector.Name}]", browser, GPALObjectType.Browser, ex);
4260 }
4261 }
4262 //else
4263 // GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Element [{webElement.TagName}] for selector [{selector.Name}] [{selector.InteractionType.ToString()}][{clickType.ToString()}] is NOT clickable.", browser, GPALObjectType.Browser);
4264
4265 Thread.Sleep(500); // CAVEAT: don't like this, but a click could be navigating and we might send the next request before the page is ready and even idle network or document.ready can fail
4266
4267 }
4275 public static void PuppeteerClick(Browser browser, GPALElement webElement, ClickType clickType, ModifierKeys modifierKeys = ModifierKeys.NONE)
4276 {
4277 switch (clickType)
4278 {
4279 case ClickType.LeftClick:
4280 browser.PuppeteerClient.LeftClick(webElement).WithModifiers(modifierKeys).Execute();
4281 break;
4282 case ClickType.LeftDoubleClick:
4283 browser.PuppeteerClient.LeftDoubleClick(webElement).WithModifiers(modifierKeys).Execute();
4284 break;
4285 case ClickType.MiddleClick:
4286 browser.PuppeteerClient.MiddleClick(webElement).WithModifiers(modifierKeys).Execute();
4287 break;
4288 case ClickType.RightClick:
4289 browser.PuppeteerClient.RightClick(webElement).WithModifiers(modifierKeys).Execute();
4290 break;
4291 }
4292 }
4301 public static void SeleniumClick(Browser browser, GPALElement webElement, ClickType clickType, ModifierKeys modifierKeys = ModifierKeys.NONE)
4302 {
4303 int retries = 2;
4304 bool retry = false;
4305 Actions action = new Actions(browser.BrowserDriver);
4306 string errorMessage = $"Failed to {clickType.ToString()} element with text [{webElement.Text}]";
4307
4308 do
4309 {
4310 try
4311 {
4312 switch (clickType)
4313 {
4314 case ClickType.LeftClick:
4315 if (ModifierKeys.NONE == modifierKeys)
4316 {
4317 webElement.iWebElement.Click();
4318 }
4319 else
4320 {
4321 if (modifierKeys.HasFlag(ModifierKeys.Alt))
4322 action.KeyDown(OpenQA.Selenium.Keys.LeftAlt).KeyDown(OpenQA.Selenium.Keys.Alt);
4323 if (modifierKeys.HasFlag(ModifierKeys.Control))
4324 action.KeyDown(OpenQA.Selenium.Keys.LeftControl).KeyDown(OpenQA.Selenium.Keys.Control);
4325 if (modifierKeys.HasFlag(ModifierKeys.Shift))
4326 action.KeyDown(OpenQA.Selenium.Keys.LeftShift).KeyDown(OpenQA.Selenium.Keys.Shift);
4327 if (modifierKeys.HasFlag(ModifierKeys.Windows))
4328 action.KeyDown(OpenQA.Selenium.Keys.Meta);
4329
4330 action.Click(webElement.iWebElement);
4331
4332 if (modifierKeys.HasFlag(ModifierKeys.Alt))
4333 action.KeyUp(OpenQA.Selenium.Keys.LeftAlt).KeyUp(OpenQA.Selenium.Keys.Alt);
4334 if (modifierKeys.HasFlag(ModifierKeys.Control))
4335 action.KeyUp(OpenQA.Selenium.Keys.LeftControl).KeyUp(OpenQA.Selenium.Keys.Control);
4336 if (modifierKeys.HasFlag(ModifierKeys.Shift))
4337 action.KeyUp(OpenQA.Selenium.Keys.LeftShift).KeyUp(OpenQA.Selenium.Keys.Shift);
4338 if (modifierKeys.HasFlag(ModifierKeys.Windows))
4339 action.KeyUp(OpenQA.Selenium.Keys.Meta);
4340
4341 ModifierKeys unsupported = modifierKeys & (ModifierKeys.Application | ModifierKeys.ScrollLock);
4342 if (ModifierKeys.NONE != unsupported)
4343 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"LeftClick: [{Browser.GetModifierKeys(unsupported)}] is not supported via Selenium/WebDriver Actions and was ignored.", browser, GPALObjectType.Browser);
4344
4345 action.Build().Perform();
4346 }
4347 break;
4348 case ClickType.LeftDoubleClick:
4349 action.DoubleClick(webElement.iWebElement).Perform();
4350 break;
4351 case ClickType.MiddleClick:
4352 {
4353 // a real middle click, button 1, the same thing ottomagic and puppeteer send.
4354 // this used to hold ctrl and left click instead, which opens a tab on a plain anchor
4355 // but does nothing on a page that handles its own clicks, and never fires auxclick
4356 PointerInputDevice mouse = new PointerInputDevice(PointerKind.Mouse, "default mouse");
4357 ActionSequence middleClick = new ActionSequence(mouse);
4358
4359 middleClick.AddAction(mouse.CreatePointerMove(webElement.iWebElement, 0, 0, TimeSpan.Zero));
4360 middleClick.AddAction(mouse.CreatePointerDown(MouseButton.Middle));
4361 middleClick.AddAction(mouse.CreatePointerUp(MouseButton.Middle));
4362
4363 ((IActionExecutor)browser.BrowserDriver).PerformActions(new List<ActionSequence> { middleClick });
4364
4365 if (ModifierKeys.NONE != modifierKeys)
4366 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Selenium cannot press [{Browser.GetModifierKeys(modifierKeys)}] with a middle click.", browser, GPALObjectType.Browser);
4367
4368 break;
4369 }
4370 case ClickType.RightClick:
4371 action.ContextClick(webElement.iWebElement).Perform();
4372 break;
4373 }
4374 retry = false;
4375 }
4376 catch (StaleElementReferenceException sereex)
4377 {
4378 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, errorMessage, webElement, GPALObjectType.Other, sereex);
4379 retry = ResolveWebElementIssue(browser, browser.CurrentUOW, ref webElement.iWebElement, sereex, ref retries, errorMessage);
4380 retries--;
4381 }
4382 catch (ElementClickInterceptedException ecieex)
4383 {
4384 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, errorMessage, webElement, GPALObjectType.Other, ecieex);
4385 if (false == GPAL.NoFallbackRecoveryActions)
4386 retry = JavaScriptClick(browser, webElement, clickType, modifierKeys);
4387 else
4388 retry = false;
4389 retries--;
4390 }
4391 catch (ElementNotVisibleException enveex)
4392 {
4393 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, errorMessage, webElement, GPALObjectType.Other, enveex);
4394 retry = ResolveWebElementIssue(browser, browser.CurrentUOW, ref webElement.iWebElement, enveex, ref retries, errorMessage);
4395 retries--;
4396 }
4397 catch (GPALException)
4398 {
4399 throw;
4400 }
4401 catch (Exception ex)
4402 {
4403 retries = 0;
4404 retry = false;
4405 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, errorMessage, webElement, GPALObjectType.Other, ex);
4406 }
4407 } while (retry && 0 < retries);
4408
4409 if (retries == 0 && false == GPAL.NoFallbackRecoveryActions)
4410 {
4411 GPAL.PublishSimpleEvent(GPALEventType.INFO, "Falling back on javascript click", webElement, GPALObjectType.Other);
4412 JavaScriptClick(browser, webElement, clickType, modifierKeys);
4413 }
4414 }
4415 // https://stackoverflow.com/questions/809057/how-do-i-programmatically-click-on-an-element-in-javascript
4416 // comment from above (not yet tested)
4417 // set third last parameter of MouseEvent to true (meaning that metaKey (CMD) button was held down when you clicked), this would not open the tab in a background tab
4423 public static bool JavaScriptClick(Browser browser, GPALElement webElement, ClickType clickType, ModifierKeys modifierKeys = ModifierKeys.NONE)
4424 {
4425 string clickString = clickType switch
4426 {
4427 ClickType.LeftClick => "click",
4428 ClickType.LeftDoubleClick => "dblclick",
4429 ClickType.RightClick => "contextmenu",
4430 ClickType.MiddleClick => "auxclick", // This is the real middle-click event
4431 _ => "click"
4432 };
4433 int button = (true == "contextmenu".Equals(clickString) ? 2 : 0);
4434 bool shiftPressed = 0 != (modifierKeys & ModifierKeys.Shift);
4435 bool controlPressed = 0 != (modifierKeys & ModifierKeys.Control);
4436 bool altPressed = 0 != (modifierKeys & ModifierKeys.Alt);
4437
4438 // Randomize click within 90% of the element's border
4439 var random = new Random();
4440 var margin = 0.05f; // 5% margin on each side (90% clickable area)
4441 var clickAreaWidth = (float)webElement.BoundingRect.Width * (1f - 2 * margin);
4442 var clickAreaHeight = (float)webElement.BoundingRect.Height * (1f - 2 * margin);
4443 var x = (float)webElement.BoundingRect.X + (float)webElement.BoundingRect.Width * margin + (float)random.NextDouble() * clickAreaWidth;
4444 var y = (float)webElement.BoundingRect.Y + (float)webElement.BoundingRect.Height * margin + (float)random.NextDouble() * clickAreaHeight;
4445
4446 if (true == browser.UseSelenium)
4447 {
4448 IWebDriver browserDriver = browser.BrowserDriver;
4449
4450 string clickScript = $@"
4451 var theEvent = document.createEvent(""MouseEvent"");
4452 theEvent.initMouseEvent(""{clickString}"", true, true, window, 0, 0, 0, {x}, {y}, {controlPressed.ToString().ToLower()}, {altPressed.ToString().ToLower()}, {shiftPressed.ToString().ToLower()}, false, 0, null);
4453 arguments[0].dispatchEvent(theEvent);
4454 ";
4455
4456 try
4457 {
4458 if (2 == button) // contextmenu will not fire off via javascript, but SHIFT+F10 will but not in headless
4459 {
4460 JavaScriptFocus(browser, webElement);
4461
4462 if (false == browser.BrowserSettings.UseHeadless)
4463 HardwareHelper.SendKey(GPAL.VK_F10, ModifierKeys.Shift);
4464 else // must use selenium in headless mode NOTE: Untested, bet this doesn't work... we only get here if this is a straight rightclick (like custom website)
4465 new Actions(browserDriver).ContextClick((IWebElement)webElement.WebElement).Perform();
4466 }
4467 else
4468 BrowserHelper.ExecuteJavaScript(browser, clickScript, webElement.WebElement); // click on element
4469 }
4470 catch (GPALException)
4471 {
4472 throw;
4473 }
4474 catch (Exception ex)
4475 {
4476 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to [{clickString}] element [{webElement.Text}]", webElement, GPALObjectType.Other, ex);
4477 return false;
4478 }
4479 }
4480 else if (true == browser.UsePuppeteer) // we should never be hear for ottomagic which is all javascript
4481 {
4482 string sessionId = browser.PuppeteerCommunicator.GetEffectiveSessionId();
4483
4484 string selector = webElement.Css;
4485
4486 string clickScript = null;
4487
4488 clickScript = $@"
4489 (() => {{
4490 try {{
4491 let el = null;
4492 const sel = `{selector.Replace("'", "\\'")}`;
4493
4494 const el = document.querySelector(sel) || document.evaluate(sel, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
4495 if (!el) return false;
4496
4497 const rect = el.getBoundingClientRect();
4498 const cx = rect.left + rect.width / 2 + window.scrollX;
4499 const cy = rect.top + rect.height / 2 + window.scrollY;
4500
4501 const opts = {{ bubbles: true, cancelable: true, clientX: cx, clientY: cy, view: window }};
4502
4503 // === LEFT CLICK (or double) ===
4504 if ({(clickType == ClickType.LeftClick || clickType == ClickType.LeftDoubleClick).ToString().ToLower()}) {{
4505 const count = {(clickType == ClickType.LeftDoubleClick ? 2 : 1)};
4506 for (let i = 0; i < count; i++) {{
4507 el.dispatchEvent(new MouseEvent('mousedown', {{...opts, button: 0, buttons: 1}}));
4508 el.dispatchEvent(new MouseEvent('mouseup', {{...opts, button: 0, buttons: 0}}));
4509 el.dispatchEvent(new MouseEvent('click', {{...opts, button: 0, detail: i+1}}));
4510 if (i === 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 80); // ~80ms between clicks
4511 }}
4512 return true;
4513 }}
4514
4515 // === MIDDLE CLICK (eBay pop-under) ===
4516 if ({(clickType == ClickType.MiddleClick).ToString().ToLower()}) {{
4517 el.dispatchEvent(new MouseEvent('mousedown', {{...opts, button: 1, buttons: 4}}));
4518 el.dispatchEvent(new MouseEvent('mouseup', {{...opts, button: 1, buttons: 0}}));
4519 el.dispatchEvent(new MouseEvent('click', {{...opts, button: 1}}));
4520 return true;
4521 }}
4522
4523 // === RIGHT CLICK (context menu) ===
4524 if ({(clickType == ClickType.RightClick).ToString().ToLower()}) {{
4525 el.dispatchEvent(new MouseEvent('mousedown', {{...opts, button: 2, buttons: 2}}));
4526 el.dispatchEvent(new MouseEvent('mouseup', {{...opts, button: 2, buttons: 0}}));
4527 el.dispatchEvent(new MouseEvent('contextmenu', {{...opts, button: 2}}));
4528 return true;
4529 }}
4530
4531 return false;
4532 }} catch(e) {{
4533 return false;
4534 }}
4535 }})();
4536 ";
4537
4538 browser.PuppeteerCommunicator.SendCommand<object>(DevToolsMethods.RuntimeEvaluate, new
4539 {
4540 expression = clickScript,
4541 returnByValue = true
4542 }, sessionId).GetAwaiter().GetResult();
4543
4544 //clickScript =
4545 // $@"
4546 // var theEvent = document.createEvent(""MouseEvent"");
4547 // theEvent.initMouseEvent(""{clickString}"", true, true, window, 0, 0, 0, {x}, {y}, {controlPressed.ToString().ToLower()}, {altPressed.ToString().ToLower()}, {shiftPressed.ToString().ToLower()}, false, 0, null);
4548 // let el = null;
4549 // const sel = `{selector.Replace("'", "\\'")}`;
4550
4551 // const el = document.querySelector(sel) || document.evaluate(sel, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
4552 // if (!el) return false;
4553 // el.dispatchEvent(theEvent);
4554 // ";
4555
4556
4557 //browser.PuppeteerCommunicator.SendCommand<object>(DevToolsMethods.RuntimeEvaluate, new
4558 //{
4559 // expression = clickScript,
4560 // returnByValue = true
4561 //}, sessionId).GetAwaiter().GetResult();
4562
4563 }
4564
4565 return true;
4566 }
4567
4568 // https://www.javascripttutorial.net/dom/css/check-if-an-element-is-visible-in-the-viewport/
4589 internal static void RefreshLocation(Browser browser, GPALElement element)
4590 {
4591 try
4592 {
4593 Rectangle live = new Rectangle(0, 0, 0, 0);
4594
4595 if (true == browser.UsePuppeteer && 0 != element.ElementBackendNodeId)
4596 // the backend node id, not ElementHandle. that one is the objectId, and this endpoint parses
4597 // what it is given as an integer, so an objectId throws inside it and the whole call comes
4598 // back as nothing - which reads here as "could not be re-read" and leaves the stale position
4599 // in place, the one thing this method exists to stop
4600 live = browser.PuppeteerClient.GetBoundingClientRect(element.ElementBackendNodeId.ToString()).Execute<Rectangle>(); // viewport, the space the screen calculation expects for puppeteer
4601 else if (true == browser.UseOttoMagic)
4602 {
4603 live = browser.MagicHelper.GetBoundingClientRect(element.Css);
4604
4605 // getBoundingClientRect measures against whichever document the element is in. a top
4606 // level element is measured against the page, and the screen calculation takes the
4607 // scroll off once, so the scroll goes back on here to meet it. an element inside an
4608 // iframe is measured against that iframe, whose own position already carries the
4609 // page scroll, so putting it on here would count it twice
4610 bool inFrame = browser.CurrentUOW?.ContextPath?.Any(context => ElementType.IFrame == context.elementType) ?? false;
4611
4612 if (0 < live.Width && 0 < live.Height && false == inFrame)
4613 live.Offset(browser.MagicHelper.WindowPageOffsetX(), browser.MagicHelper.WindowPageOffsetY());
4614 }
4615 else if (null != element.WebElement)
4616 live = new Rectangle(element.WebElement.Location, element.WebElement.Size); // selenium reads these live, in document coordinates
4617
4618 if (0 < live.Width && 0 < live.Height)
4619 {
4620 element.Location = live.Location;
4621 element.Size = live.Size;
4622 element.BoundingRect = new ClientRectangle(element.Location, element.Size);
4623 }
4624 else
4625 // the position on file is where the element was when it was found. after a scroll that
4626 // is not where it is, and a hardware click aimed there lands on whatever moved into it
4627 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Could not re-read where [{element.TagName}][{element.Css}] is, so it is still where it was when it was found", browser, GPALObjectType.Browser);
4628 }
4629 catch (GPALException)
4630 {
4631 throw;
4632 }
4633 catch (Exception ex)
4634 {
4635 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Could not re-read where [{element.TagName}] is, using the reading from when it was found", browser, GPALObjectType.Browser, ex);
4636 }
4637 }
4638
4651 internal static bool PointHitsElement(Browser browser, GPALElement element, Point screenPoint)
4652 {
4653 bool retVal = false;
4654
4655 try
4656 {
4657 // elementFromPoint asks in viewport coordinates, so the browser's content area comes back off
4658 Rectangle content = GetWindowRectangle(browser);
4659 int x = screenPoint.X - content.X;
4660 int y = screenPoint.Y - content.Y;
4661
4662 if (true == browser.UsePuppeteer)
4663 retVal = browser.BrowserSettings.PuppeteerCommunicator.ElementFromPoint(element, x, y).GetAwaiter().GetResult();
4664 else if (true == browser.UseOttoMagic)
4665 retVal = browser.MagicHelper.ElementFromPoint(element.Css, x, y);
4666 else
4667 {
4668 var result = BrowserHelper.ExecuteJavaScriptObj(
4669 "var elem = arguments[0]; " +
4670 "var hit = document.elementFromPoint(arguments[1], arguments[2]); " +
4671 "return hit === elem || elem.contains(hit);", browser, element.WebElement, x, y);
4672
4673 retVal = null != result && true == Convert.ToBoolean(result);
4674 }
4675
4676 //GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Asked what is at viewport [{x}, {y}] (screen [{screenPoint.X}, {screenPoint.Y}], content origin [{content.X}, {content.Y}]) for [{element.TagName}][{element.Css}]: [{retVal}]", browser, GPALObjectType.Browser);
4677 }
4678 catch (GPALException)
4679 {
4680 throw;
4681 }
4682 catch (Exception ex)
4683 {
4684 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Could not ask what is at [{screenPoint.X}, {screenPoint.Y}] for [{element.TagName}]", browser, GPALObjectType.Browser, ex);
4685 }
4686
4687 return retVal;
4688 }
4689
4690 // how far in from the anchored edge to try, in pixels, before falling back to fractions of the region.
4691 // an element far wider than the window is anchored to something and overflowing away from it, and the
4692 // control that does anything sits at the anchored edge while the rest is empty box. a percentage of a
4693 // 3782px element says nothing about where a button a few dozen pixels wide is, and 2% of it is close
4694 // enough to the edge to land half off. a real control is at least this far in
4695 private static readonly int[] hitTestInsets = { 24, 40, 60, 12 };
4696
4697 // and failing all of those, across the region, so an element that is not anchored the way this assumes
4698 // still gets tried everywhere rather than only at one end
4699 private static readonly float[] hitTestFractions = { 0.75f, 0.50f, 0.25f, 0.10f };
4700
4701 public static bool IsVisibleInViewport(Browser browser, GPALElement element)
4702 {
4703 bool retval = false; // a check that cannot be answered is not a yes - callers use this to decide whether a coordinate click is safe
4704
4705 if (true == browser.UsePuppeteer)
4706 retval = browser.PuppeteerClient.IsVisibleInViewport(element).Execute<bool>();
4707 else if (true == browser.UseOttoMagic)
4708 retval = browser.MagicHelper.IsVisibleInViewport(element.Css);
4709 else
4710 {
4711 IWebDriver browserDriver = browser.BrowserDriver;
4712
4713 var result = BrowserHelper.ExecuteJavaScriptObj(
4714 "var elem = arguments[0]; " +
4715 "const rect = elem.getBoundingClientRect(); " +
4716 "return ( " +
4717 " rect.top >= 0 && " +
4718 " rect.left >= 0 && " +
4719 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && " +
4720 " rect.right <= (window.innerWidth || document.documentElement.clientWidth) " +
4721 ");", browser, element.WebElement);
4722
4723 if (null != result)
4724 retval = (bool)result;
4725 }
4726
4727 return retval;
4728 }
4735 public static bool IsEndOfPage(Browser browser, GPALElement element)
4736 {
4737 bool retval = true;
4738
4739 if (true == browser.UsePuppeteer)
4740 retval = browser.PuppeteerClient.IsEndOfPage().Execute<bool>();
4741 else if (true == browser.UseOttoMagic)
4742 retval = browser.MagicHelper.IsEndOfPage();
4743 else
4744 {
4745 IWebDriver browserDriver = browser.BrowserDriver;
4746 // documentElement.scrollHeight, not body.offsetHeight - body is shorter than the scrollable area
4747 // whenever html is the scroll container or body carries margins, which reports end of page on page one
4748 var result = BrowserHelper.ExecuteJavaScriptObj("if ((window.innerHeight + window.scrollY) >= document.documentElement.scrollHeight) { return true; } else { return false; }", browser, null);
4749
4750 if (null != result)
4751 retval = (bool)result;
4752 }
4753
4754 return retval;
4755 }
4765 private static bool SettledIsVisibleInViewport(Browser browser, GPALElement element)
4766 {
4767 bool visible = IsVisibleInViewport(browser, element);
4768 int checksLeft = VisibilitySettleChecks;
4769
4770 while (false == visible && 0 < checksLeft--)
4771 {
4772 Thread.Sleep(VisibilitySettleMs);
4773 visible = IsVisibleInViewport(browser, element);
4774 }
4775
4776 return visible;
4777 }
4794 private static bool IsBiggerThanViewport(Size elementSize, Rectangle viewport)
4795 {
4796 return elementSize.Width > viewport.Width || elementSize.Height > viewport.Height;
4797 }
4798 internal static bool ScrollUntilVisible(Browser browser, GPALElement element)
4799 {
4800 bool visible = SettledIsVisibleInViewport(browser, element);
4801
4802 // IsVisibleInViewport wants the whole element inside the viewport, so an element bigger than the
4803 // window can never answer yes and paging the document looking for one only wastes the time
4804 if (false == visible && true == IsBiggerThanViewport(element.Size, GetWindowRectangle(browser)))
4805 visible = true;
4806
4807 if (false == visible)
4808 {
4809 int pagesLeft = MaxScrollPages;
4810
4811 _ = browser.PageTop;
4812 visible = SettledIsVisibleInViewport(browser, element);
4813
4814 // page cap as well as end of page - a site whose scroll container we cannot measure would
4815 // otherwise walk the whole document a screen at a time on every failed click
4816 while (false == visible && 0 < pagesLeft-- && false == IsEndOfPage(browser, element))
4817 {
4818 _ = browser.PageDown;
4819 visible = SettledIsVisibleInViewport(browser, element);
4820 }
4821
4822 if (false == visible)
4823 {
4824 _ = browser.PageTop;
4825 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Could not scroll element [{element.TagName}][{element.Css}] into the viewport. Left the page at the top and continuing.", browser, GPALObjectType.Browser);
4826 }
4827 }
4828
4829 return visible;
4830 }
4831
4837 private static IntPtr GetWindowHandle(BrowserType browserType)
4838 {
4839 IntPtr hWnd = IntPtr.Zero;
4840 Process[] processes = null;
4841
4842 if (BrowserType.Chrome == browserType)
4843 processes = Process.GetProcessesByName("chrome");
4844 else if (BrowserType.Edge == browserType)
4845 processes = Process.GetProcessesByName("msedge");
4846 else if (BrowserType.FireFox == browserType)
4847 processes = Process.GetProcessesByName("firefox");
4848
4849 if (processes.Length > 0)
4850 {
4851 foreach (Process proc in processes)
4852 {
4853 if (proc.MainWindowHandle == IntPtr.Zero)
4854 continue;
4855 else
4856 {
4857 hWnd = proc.MainWindowHandle;
4858 break;
4859 }
4860 }
4861 }
4862 return hWnd;
4863 }
4864
4876 // Set true by HardwareClick when the element's screen placement was questionable (odd/off-screen coords,
4877 // so we could not do a real hardware click). HardwareFillInFrom reads it to refuse to type - otherwise a
4878 // failed click leaves focus wherever it was (e.g. the browser address bar) and keystrokes leak there.
4879 // ThreadStatic so concurrent browser threads do not clobber each other's last-click state.
4880 [ThreadStatic] private static bool _lastPlacementQuestionable;
4881
4882 // set by HardwareMoveTo when it had to clamp the point into the element's visible part, read by the click
4883 // that follows so the message says where it actually clicked. null when nothing had to be done
4884 [ThreadStatic] private static string _lastClampedPoint;
4885
4886 // the outer window as GetWindowRectangle last saw it, position and size. that method answers with the
4887 // content area, which is what a click has to be computed against, but what a workflow sets with
4888 // WithWindowSize is the outer window - so that is what is worth quoting back to it, in the same
4889 // x, y, width, height order the Rectangle it takes is written in
4890 [ThreadStatic] private static Rectangle _lastOuterWindow;
4891
4892 public static void HardwareClick(Browser browser, dynamic element, ClickType clickType, ModifierKeys modifierKeys, Selector selector, bool isClickable = false)
4893 {
4894 Rectangle rect, iframeRect = new Rectangle(0, 0, 0, 0);
4895 bool clicked = false;
4896 _lastPlacementQuestionable = false;
4897 _lastClampedPoint = null;
4898
4899 try
4900 {
4901 BrowserHelper.TopBrowser(browser, false);
4902
4903 if (ModifierKeys.NONE != modifierKeys)
4904 HardwareHelper.PressModifierKey(modifierKeys);
4905
4906 // element location calculations handled in hardwaremoveto, we use the rectangle computed there
4907 // will perform simulate mouse move
4908 rect = HardwareMoveTo(browser, element, selector);
4909
4910 // the point has to land inside the browser window on every side. checking only for positive
4911 // coordinates lets an element below the fold through with a large positive Y, and the click then
4912 // lands on the taskbar or whatever window is behind. an image match is already a verified screen
4913 // position, so it is the one thing allowed past
4914 Rectangle browserRect = GetWindowRectangle(browser);
4915 bool insideBrowser = 0 < rect.Width && 0 < rect.Height
4916 && rect.X > browserRect.Left && rect.X < browserRect.Right
4917 && rect.Y > browserRect.Top && rect.Y < browserRect.Bottom;
4918
4919 if ("GPALElement" == element.TagName || true == insideBrowser)
4920 {
4921 // will do a jump to x,y
4922 HardwareHelper.HardwareClick(rect.X, rect.Y, clickType);
4923 clicked = true;
4924 }
4925 else
4926 {
4927 // The element's screen placement is not trustworthy (off-screen/odd coords) so we could not
4928 // perform a real hardware click. Flag it so a following hardware fill-in refuses to type.
4929 _lastPlacementQuestionable = true;
4930
4931 if (false == GPAL.NoFallbackRecoveryActions)
4932 {
4933 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Element [{element.TagName}] for selector [{selector?.Name}] computed to [{rect.X}, {rect.Y}, {rect.Width}, {rect.Height}], outside the browser window [{browserRect.Left}, {browserRect.Top}, {browserRect.Right}, {browserRect.Bottom}]. No hardware click, falling back on javascript click.", browser, GPALObjectType.Browser);
4934
4935 if (ModifierKeys.NONE != modifierKeys)
4936 HardwareHelper.ReleaseModifierKeys(modifierKeys);
4937
4938 if (true == browser.UseOttoMagic)
4939 browser.MagicHelper.LeftClick(element.Css);
4940 else if (true == browser.UsePuppeteer)
4941 browser.PuppeteerClient.LeftClick(element).Execute();
4942 else
4943 JavaScriptClick(browser, element, clickType, modifierKeys);
4944
4945 clicked = true;
4946 }
4947 else
4948 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Unable to get screen coordinates for webelement, continuing.", browser, GPALObjectType.Browser);
4949 }
4950 }
4951 catch
4952 {
4953 }
4954 finally
4955 {
4956 // a clamped point is worth saying out loud, and worth saying as a warning: the click landed, but
4957 // the workflow could name that offset itself and stop needing the correction
4958 GPAL.PublishSimpleEvent(null == _lastClampedPoint ? GPALEventType.INFO : GPALEventType.WARNING,
4959 $"Element [{element.TagName}] for selector [{selector.Name}] [{selector.InteractionType.ToString()}][{clickType.ToString()}]{(true == clicked ? "" : " [NOT]")} clicked{(null == _lastClampedPoint ? "" : " " + _lastClampedPoint)}.", browser, GPALObjectType.Browser);
4960
4961 if (ModifierKeys.NONE != modifierKeys)
4962 HardwareHelper.ReleaseModifierKeys(modifierKeys);
4963
4964 // BrowserHelper.TopBrowser(browser, true); // just move the mouse to a safe area off the browser area that can cause hover issues
4965 }
4966 }
4967
4974 public static void DragAndDrop(IBrowser browser, ModifierKeys modifierKeys = ModifierKeys.NONE)
4975 {
4976 var driver = browser.BrowserDriver;
4977 Rectangle iframeRect = new Rectangle(0, 0, 0, 0);
4978
4979 List<GPALElement> matchedElements;
4980
4981 foreach (Selector selector in ((Browser)browser).CurrentUOW.WithSelectorList)
4982 {
4983 if (SelectorType.Selector != selector.SelectorType)
4984 continue; // we don't look up data literals
4985
4986 ReadOnlyCollection<GPALElement> elems = ElementHelper.FindWebElements((Browser)browser, ((Browser)browser).CurrentUOW, selector, out bool matchedAll, out matchedElements);
4987
4988 // FindWebElements calls CallIF handlers which can handle an action and delete a node, check here before moving on
4989 if (true == selector.DeleteMe || null == elems || 0 == elems.Count)
4990 {
4991 PublishToEventHandler((Browser)browser, selector, elems?.ToList());
4992 continue;
4993 }
4994
4995 if (null != matchedElements)
4996 elems = new ReadOnlyCollection<GPALElement>(matchedElements);
4997
4998 int rowCount = 0;
4999
5000 foreach (GPALElement elem in elems)
5001 {
5002 if (true == ((Browser)browser).BrowserSettings.UseHardware || InteractionType.Hardware == selector.InteractionType)
5003 {
5004 // remote-desktop / image-matched pseudo-element - no DOM handle, fall back to hardware emulation
5005 // hold the mouse down, then walk the cursor smoothly (wind mouse) to the drop point before releasing -
5006 // a remote desktop session can drop the drag if the cursor jumps instantly to the target
5007 try
5008 {
5009 BrowserHelper.TopBrowser((Browser)browser, false);
5010
5011 // element location calculations handled in hardwaremoveto (iframe nesting, browser chrome,
5012 // random in-element point when no offset), we use the rectangle computed there as the grab point
5013 Rectangle rect = HardwareMoveTo((Browser)browser, elem, selector);
5014
5015 if ("GPALElement" == elem.TagName || (0 <= rect.Width && 0 <= rect.Height && 0 < rect.X && 0 < rect.Y))
5016 {
5017 int endX = rect.X + selector.DeltaX;
5018 int endY = rect.Y + selector.DeltaY;
5019
5020 HardwareHelper.MouseDown(rect.X, rect.Y, ClickType.LeftClick);
5021 HardwareHelper.MoveMouse(endX, endY, 4, 4, 0.5);
5022 HardwareHelper.MouseUp(endX, endY, ClickType.LeftClick);
5023 }
5024 else
5025 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Odd screen coordinates [{rect.X}, {rect.Y}, {rect.Width}, {rect.Height}] for hardware drag and drop, skipping element.", browser, GPALObjectType.Browser);
5026 }
5027 finally
5028 {
5029 // BrowserHelper.TopBrowser((Browser)browser, true); // just move the mouse to a safe area off the browser area that can cause hover issues
5030 }
5031 }
5032 else if (true == browser.UseOttoMagic)
5033 {
5034 browser.MagicHelper.DragAndDrop(elem.Css, selector.DeltaX, selector.DeltaY, selector.OffsetX, selector.OffsetY);
5035 }
5036 else if (true == browser.UsePuppeteer)
5037 {
5038 browser.PuppeteerClient.DragAndDrop(elem.ElementHandle).WithDeltaX(selector.DeltaX).WithDeltaY(selector.DeltaY).WithOffsetX(selector.OffsetX).WithOffsetY(selector.OffsetY).Execute();
5039 }
5040 else if (true == browser.UseSelenium) // real WebElement, selenium has a handle to drag
5041 {
5042 // firefox's native drag service only starts from real OS input, so selenium's synthesized
5043 // pointer events never fire dragstart on an HTML5 draggable and the drag silently does nothing.
5044 // chrome and edge inject low enough that they do fire it, so only firefox needs the javascript
5045 // path - and only for HTML5 draggables, since mouse driven dragging (map panning, jquery ui)
5046 // is moved by plain mousedown/mousemove/mouseup and Actions drives that fine everywhere.
5047 if (BrowserType.FireFox == ((Browser)browser).BrowserSettings.BrowserType
5048 && "true" == elem.WebElement?.GetAttribute("draggable"))
5049 JavaScriptDragAndDrop((Browser)browser, elem, selector.DeltaX, selector.DeltaY, selector.OffsetX, selector.OffsetY);
5050 else
5051 new Actions(driver).ClickAndHold(elem.WebElement).MoveByOffset(selector.DeltaX, selector.DeltaY).Release().Perform();
5052 }
5053
5054 if (++rowCount >= ((Browser)browser).CurrentUOW.WithAllThatMatch)
5055 break;
5056 }
5057 }
5058 }
5059
5073 public static bool JavaScriptDragAndDrop(Browser browser, GPALElement element, int deltaX, int deltaY, int offsetX, int offsetY)
5074 {
5075 // this can happen with a gpalelement
5076 if (null == element?.WebElement)
5077 return false;
5078
5079 ScrollIntoView(browser, element);
5080
5081 // grab point is the element center plus the offset, matching the puppeteer drag and drop
5082 string javaScript = @"
5083 var src = arguments[0];
5084 var offsetX = arguments[1], offsetY = arguments[2];
5085 var deltaX = arguments[3], deltaY = arguments[4];
5086
5087 var rect = src.getBoundingClientRect();
5088 var startX = rect.left + rect.width / 2 + offsetX;
5089 var startY = rect.top + rect.height / 2 + offsetY;
5090 var endX = startX + deltaX;
5091 var endY = startY + deltaY;
5092
5093 var target = document.elementFromPoint(endX, endY);
5094
5095 if (!target)
5096 return 'nothing at drop point ' + endX + ',' + endY;
5097
5098 var dt = new DataTransfer();
5099
5100 function fire(type, el, x, y) {
5101 el.dispatchEvent(new DragEvent(type, {
5102 bubbles: true, cancelable: true, composed: true,
5103 clientX: x, clientY: y, dataTransfer: dt
5104 }));
5105 }
5106
5107 fire('dragstart', src, startX, startY);
5108 fire('dragenter', target, endX, endY);
5109 fire('dragover', target, endX, endY);
5110 fire('drop', target, endX, endY);
5111 fire('dragend', src, endX, endY);
5112
5113 return 'ok';
5114 ";
5115
5116 string result = BrowserHelper.ExecuteJavaScript(browser, javaScript, element.WebElement, offsetX, offsetY, deltaX, deltaY);
5117
5118 if ("ok" != result?.Trim())
5119 {
5120 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"HTML5 drag and drop did not complete for selector [{element.Selector?.Name}] [{result}].", browser, GPALObjectType.Browser);
5121 return false;
5122 }
5123
5124 return true;
5125 }
5135 public static void JavaScriptHover(Browser browser, GPALElement element)
5136 {
5137 // this can happen with a gpalelement
5138 if (null == element.WebElement)
5139 return;
5140
5141 ScrollIntoView(browser, element);
5142
5143 // dispatch a mouseover event on the targetted element which is hover, not focus
5144 String javaScript = "var evObj = document.createEvent('MouseEvents');"
5145 + "evObj.initMouseEvent(\"mouseover\",true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);"
5146 + "arguments[0].dispatchEvent(evObj);";
5147
5148 BrowserHelper.ExecuteJavaScript(browser, javaScript, element.WebElement);
5149 }
5150 // for some reason we are having trouble getting the msedge bounding rect
5157 private static Rectangle GetWindowRect(string windowTitle)
5158 {
5159 IntPtr hWnd = IntPtr.Zero;
5160 foreach (Process pList in Process.GetProcesses())
5161 {
5162 if (pList.MainWindowTitle.Contains(windowTitle))
5163 {
5164 hWnd = pList.MainWindowHandle;
5165 break;
5166 }
5167 }
5168
5169 if (hWnd != IntPtr.Zero)
5170 {
5171 RECT rect;
5172 GetWindowRect(hWnd, out rect);
5173 return new Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top);
5174 }
5175 else
5176 {
5177 return Rectangle.Empty;
5178 }
5179 }
5180
5187 [DllImport("user32.dll")]
5188 [return: MarshalAs(UnmanagedType.Bool)]
5189 private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
5190
5191 [StructLayout(LayoutKind.Sequential)]
5192 public struct RECT
5193 {
5194 public int Left;
5195 public int Top;
5196 public int Right;
5197 public int Bottom;
5198 }
5199
5213 private static void RestoreFrameContext(Browser browser)
5214 {
5215 if (null == browser.CurrentUOW?.ContextPath)
5216 return;
5217
5218 foreach (UnitOfWork.WebElementWithType contextType in browser.CurrentUOW.ContextPath)
5219 if (ElementType.IFrame == contextType.elementType && null != contextType.gPalElement?.WebElement)
5220 browser.BrowserDriver.SwitchTo().Frame((IWebElement)contextType.gPalElement.WebElement);
5221 }
5230 public static Rectangle GetWindowRectangle(Browser browser, out Rectangle outerWindow)
5231 {
5232 Rectangle retVal = GetWindowRectangle(browser);
5233
5234 // written by the read above, and thread static, so it belongs to the workflow that just asked
5235 outerWindow = _lastOuterWindow;
5236
5237 return retVal;
5238 }
5239
5240 public static Rectangle GetWindowRectangle(Browser browser)
5241 {
5242 // Primary: Use JS to get screen pos and sizes (reliable, avoids UIA flakiness)
5243 bool jsSuccess = false;
5244 int screenX = 0, screenY = 0, outerHeight = 0, innerHeight = 0, outerWidth = 0, innerWidth = 0;
5245
5246 try
5247 {
5248 bool withReturn = !browser.UsePuppeteer;
5249 string prefix = withReturn ? "return " : "";
5250 dynamic value = null;
5251
5252 if (true == browser.UseOttoMagic)
5253 {
5254 Rectangle rect = browser.MagicHelper.GetWindowRectangle();
5255 // { y: window.innerHeight, x: window.innerWidth, height: window.outerHeight, width: window.outerWidth }
5256 outerHeight = rect.Height;
5257 innerHeight = rect.Y;
5258 outerWidth = rect.Width;
5259 innerWidth = rect.X;
5260 screenX = browser.MagicHelper.WindowScreenLeft();
5261 screenY = browser.MagicHelper.WindowScreenTop();
5262 jsSuccess = true;
5263 }
5264 else if (true == browser.UsePuppeteer) // NOTE: not using javascript for now
5265 {
5266 outerHeight = browser.PuppeteerClient.WindowOuterHeight().Execute<int>();
5267 innerHeight = browser.PuppeteerClient.WindowInnerHeight().Execute<int>();
5268 outerWidth = browser.PuppeteerClient.WindowOuterWidth().Execute<int>();
5269 innerWidth = browser.PuppeteerClient.WindowInnerWidth().Execute<int>();
5270 screenX = browser.PuppeteerClient.WindowScreenLeft().Execute<int>();
5271 screenY = browser.PuppeteerClient.WindowScreenTop().Execute<int>();
5272 jsSuccess = true;
5273 }
5274 else
5275 {
5276 string jsScript = $"{prefix} ["
5277 + "window.screenLeft, "
5278 + "window.screenTop, "
5279 + "window.outerHeight, window.innerHeight, window.outerWidth, window.innerWidth]";
5280
5281 // these describe the browser window, not whatever frame we happen to be inside. selenium runs
5282 // script in the frame the driver is switched into, where innerHeight is the frame's own
5283 // viewport - that makes the computed chrome height enormous and drops the content area near
5284 // the bottom of the screen. measure against the top document and put the context back.
5285 bool inFrame = browser.CurrentUOW?.ContextPath?.Any(context => ElementType.IFrame == context.elementType) ?? false;
5286
5287 try
5288 {
5289 if (true == inFrame)
5290 browser.BrowserDriver.SwitchTo().DefaultContent();
5291
5292 value = BrowserHelper.ExecuteJavaScriptObj(jsScript, browser, null);
5293 }
5294 finally
5295 {
5296 if (true == inFrame)
5297 RestoreFrameContext(browser);
5298 }
5299 IList<object> values;
5300
5301 if (value is Newtonsoft.Json.Linq.JArray jArray)
5302 {
5303 values = jArray.Select(token => (object)token).ToList();
5304 }
5305 else if (value is IList<object> list)
5306 {
5307 values = list;
5308 }
5309 else
5310 {
5311 // Fallback if unexpected format (e.g., dict); assume defaults
5312 values = new List<object> { 0, 0, 0, 0, 0, 0 };
5313 }
5314
5315 screenX = values.Count > 0 ? Convert.ToInt32(values[0]) : 0;
5316 screenY = values.Count > 1 ? Convert.ToInt32(values[1]) : 0;
5317 outerHeight = values.Count > 2 ? Convert.ToInt32(values[2]) : 0;
5318 innerHeight = values.Count > 3 ? Convert.ToInt32(values[3]) : 0;
5319 outerWidth = values.Count > 4 ? Convert.ToInt32(values[4]) : 0;
5320 innerWidth = values.Count > 5 ? Convert.ToInt32(values[5]) : 0;
5321 jsSuccess = true;
5322 }
5323
5324 if (jsSuccess && innerWidth > 0 && innerHeight > 0)
5325 {
5326 _lastOuterWindow = new Rectangle(screenX, screenY, outerWidth, outerHeight);
5327
5328 int navHeight = outerHeight - innerHeight;
5329 int navWidth = outerWidth - innerWidth;
5330 // the window's border is the same width on the left, the right and the bottom, so half the
5331 // width the content area loses is the border beside it. the height it loses is the chrome
5332 // above it plus that same border below it, and only the chrome sits above the content, so
5333 // the border comes back off. charging the bottom border to the top is what drops every
5334 // hardware click by a border's width
5335 int border = navWidth / 2;
5336 int contentLeft = screenX + border;
5337 int contentTop = screenY + navHeight - border;
5338 return new Rectangle(contentLeft, contentTop, innerWidth, innerHeight);
5339 }
5340 }
5341 catch (GPALException)
5342 {
5343 throw;
5344 }
5345 catch (Exception ex)
5346 {
5347 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"JS execution failed for browser [{browser.BrowserSettings.BrowserType}]", browser, GPALObjectType.Browser, ex);
5348 }
5349
5350 // a browser that cannot answer this cannot be automated either: every read above is the page
5351 // describing itself, and a page that will not run script has already stopped the workflow.
5352 // nothing is guessed here in its place
5353 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to get rectangle for [{browser.BrowserSettings.BrowserType}].", browser, GPALObjectType.Browser);
5354
5355 return new Rectangle(0, 0, 0, 0);
5356 }
5364 public static Rectangle GetWindowOuterRectangle(Browser browser)
5365 {
5366 int screenX = 0, screenY = 0, outerWidth = 0, outerHeight = 0;
5367
5368 try
5369 {
5370 if (true == browser.UseOttoMagic)
5371 {
5372 // { y: innerHeight, x: innerWidth, height: outerHeight, width: outerWidth }
5373 Rectangle rect = browser.MagicHelper.GetWindowRectangle();
5374 outerWidth = rect.Width;
5375 outerHeight = rect.Height;
5376 screenX = browser.MagicHelper.WindowScreenLeft();
5377 screenY = browser.MagicHelper.WindowScreenTop();
5378 }
5379 else if (true == browser.UsePuppeteer)
5380 {
5381 outerWidth = browser.PuppeteerClient.WindowOuterWidth().Execute<int>();
5382 outerHeight = browser.PuppeteerClient.WindowOuterHeight().Execute<int>();
5383 screenX = browser.PuppeteerClient.WindowScreenLeft().Execute<int>();
5384 screenY = browser.PuppeteerClient.WindowScreenTop().Execute<int>();
5385 }
5386 else
5387 {
5388 var value = BrowserHelper.ExecuteJavaScriptObj("return [window.screenLeft, window.screenTop, window.outerWidth, window.outerHeight]", browser, null);
5389 IList<object> values;
5390
5391 if (value is Newtonsoft.Json.Linq.JArray jArray)
5392 values = jArray.Select(token => (object)token).ToList();
5393 else if (value is IList<object> list)
5394 values = list;
5395 else
5396 values = new List<object> { 0, 0, 0, 0 };
5397
5398 screenX = values.Count > 0 ? Convert.ToInt32(values[0]) : 0;
5399 screenY = values.Count > 1 ? Convert.ToInt32(values[1]) : 0;
5400 outerWidth = values.Count > 2 ? Convert.ToInt32(values[2]) : 0;
5401 outerHeight = values.Count > 3 ? Convert.ToInt32(values[3]) : 0;
5402 }
5403
5404 return new Rectangle(screenX, screenY, outerWidth, outerHeight);
5405 }
5406 catch (GPALException)
5407 {
5408 throw;
5409 }
5410 catch (Exception ex)
5411 {
5412 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to get outer window rectangle for [{browser.BrowserSettings.BrowserType}]", browser, GPALObjectType.Browser, ex);
5413 return new Rectangle(0, 0, 0, 0);
5414 }
5415 }
5422 public static Dictionary<string, object> GetCssAttributes(Browser browser, IWebElement webElement)
5423 {
5424 var script = @"
5425 const el = arguments[0];
5426 const styles = window.getComputedStyle(el);
5427 var props = {};
5428
5429 // 2. Values that are ""noise"" - we never want them in the result
5430 const EMPTY_VALUES = new Set([
5431 '', 'none', 'normal', 'inherit', 'initial', 'unset',
5432 'auto', '0px', '0', 'transparent'
5433 ]);
5434
5435 // 3. The only properties we ever care about
5436 const RELEVANT = new Set([
5437 'display', 'visibility', 'opacity', 'pointer-events', 'overflow', 'clip-path',
5438 'position', 'top', 'left', 'right', 'bottom', 'z-index',
5439 'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
5440 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
5441 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
5442 'border', 'border-width', 'border-style', 'border-color', 'box-sizing',
5443 'flex', 'flex-direction', 'flex-wrap', 'flex-grow', 'flex-shrink', 'flex-basis',
5444 'justify-content', 'align-items', 'align-self', 'order',
5445 'grid', 'grid-template', 'grid-area', 'grid-column', 'grid-row',
5446 'transform', 'transform-origin', 'transition', 'animation',
5447 'cursor', 'user-select', 'content', 'filter'
5448 ]);
5449
5450 // 4. Iterate only the properties that exist on the style object
5451 for (let i = 0; i < styles.length; i++) {
5452 const prop = styles[i]; // e.g. ""display""
5453 if (!RELEVANT.has(prop)) { continue; } // skip everything else
5454
5455 const value = styles.getPropertyValue(prop).trim(); // ""block"", ""0.5"", etc.
5456
5457 // Skip empty / default values
5458 if (!value || EMPTY_VALUES.has(value)) { continue; }
5459
5460 props[prop] = value;
5461 }
5462 return props;
5463 ";
5464
5465 var dict = BrowserHelper.ExecuteJavaScriptObj(script, browser, webElement) as Dictionary<string, object>;
5466
5467 return dict?.ToDictionary(k => k.Key, v => v.Value ?? "");
5468 }
5473 public static Dictionary<string, object> GetDomAttributes(
5474 Browser browser,
5475 IWebElement webElement)
5476 {
5477 const string script = @"
5478 const cleanText = (str) => (str || '').replace(/\s+/g, ' ').trim();
5479 const el = arguments[0];
5480 const attrs = el.attributes;
5481 const result = {};
5482 for (let i = 0; i < attrs.length; i++) {
5483 const a = attrs[i];
5484 result[a.name] = a.value; // value is always a string or empty
5485 }
5486 // Also add a few handy built-in properties that are not attributes
5487 result['id'] = el.id;
5488 result['className'] = el.className;
5489 result['tagName'] = el.tagName;
5490 result['innerText'] = cleanText(el.innerText);
5491 result['textContent'] = cleanText(el.textContent);
5492 return result;
5493 ";
5494
5495 var raw = BrowserHelper.ExecuteJavaScriptObj(script, browser, webElement)
5496 as Dictionary<string, object>;
5497
5498 // Normalise null -> empty string (mirrors your CSS method)
5499 return raw?.ToDictionary(
5500 kvp => kvp.Key,
5501 kvp => kvp.Value ?? ""
5502 ) ?? new Dictionary<string, object>();
5503 }
5508 public static Dictionary<string, object> GetDomProperties(
5509 Browser browser,
5510 IWebElement webElement)
5511 {
5512 const string script = @"
5513 const el = arguments[0];
5514 const props = {};
5515
5516 if (""textContent"" in el) props.textContent = el.textContent;
5517 if (""innerHTML"" in el) props.innerHTML = el.innerHTML;
5518 if (""innerText"" in el) props.innerText = el.innerText;
5519 if (""outerHTML"" in el) props.outerHTML = el.outerHTML;
5520 if (""className"" in el) props.className = el.className;
5521 if (""disabled"" in el) props.disabled = el.disabled;
5522
5523 return props;
5524 ";
5525
5526 var raw = BrowserHelper.ExecuteJavaScriptObj(script, browser, webElement)
5527 as Dictionary<string, object>;
5528
5529 // Normalize null -> "" (consistent with your CSS method)
5530 return raw?.ToDictionary(
5531 kvp => kvp.Key,
5532 kvp => kvp.Value ?? ""
5533 ) ?? new Dictionary<string, object>();
5534 }
5547 internal static bool ResolveWebElementIssue(Browser browser, UnitOfWork currentUOW, ref dynamic webOrGpalElement, Exception exception, ref int retries, string errorMessage)
5548 {
5549 // no fallback
5550 if (true == GPAL.NoFallbackRecoveryActions)
5551 return false;
5552
5553 bool shouldRetry = false;
5554 IWebElement iWebElement;
5555 GPALElement gPalElement = null;
5556
5557 if (webOrGpalElement is GPALElement)
5558 {
5559 gPalElement = webOrGpalElement;
5560 iWebElement = gPalElement.WebElement;
5561 }
5562 else
5563 {
5564 iWebElement = webOrGpalElement;
5565 gPalElement = new GPALElement() { WebElement = iWebElement };
5566 }
5567
5568 // Log the recovery attempt
5569 string recoveryMessage = $"Attempting recovery for [{exception.GetType().Name}]. Retries left: [{retries}]";
5570 GPAL.PublishSimpleEvent(GPALEventType.INFO, recoveryMessage, iWebElement, GPALObjectType.Other);
5571
5572 try
5573 {
5574 if (exception is StaleElementReferenceException)
5575 {
5576 // clear the cache, update GPALElement.IWebElement - tested, recovery works for stale cache
5577 if (null != currentUOW.CurrentSelector.WebSelectorMatchedResults || null != currentUOW.CurrentSelector.WebSelectorFoundResults)
5578 {
5579 currentUOW.CurrentSelector.WebSelectorMatchedResults = null;
5580 currentUOW.CurrentSelector.WebSelectorFoundResults = null;
5581 }
5582 else
5583 // Step 2: Refresh page (no locator available)
5584 _ = browser.Refresh;
5585
5586 if (null != webOrGpalElement)
5587 {
5588 var elems = FindWebElements(browser, currentUOW, currentUOW.CurrentSelector, out bool matchedAll, out List<GPALElement> matched, null, false);
5589
5590 if (0 < elems.Count())
5591 webOrGpalElement = elems[0].WebElement;
5592 }
5593 shouldRetry = true;
5594 }
5595 else if (exception is ElementClickInterceptedException)
5596 {
5597 // Step 2: Scroll element into view
5598 ElementHelper.MoveTo(gPalElement, currentUOW.CurrentSelector.OffsetX, currentUOW.CurrentSelector.OffsetY, browser);
5599
5600 // Step 3: Wait for element to be clickable (visible and enabled)
5601 if (false == (shouldRetry = IsElementClickable(iWebElement)))
5602 {
5603 // CAVEAT: this uses a heuristic, no guarantees
5604 // Step 4: Handle overlays (modals, popups)
5605 try
5606 {
5607 var overlays = browser.BrowserDriver.FindElements(By.XPath("//div[contains(@class, 'modal') or contains(@class, 'overlay') or contains(@class, 'popup')]"));
5608 if (overlays.Any())
5609 {
5610 foreach (var overlay in overlays)
5611 {
5612 if (overlay.Displayed)
5613 {
5614 var closeButton = browser.BrowserDriver.FindElements(By.XPath(".//button[contains(@class, 'close') or contains(text(), 'Close') or @aria-label='Close']"));
5615 if (closeButton.Any())
5616 {
5617 closeButton.First().Click();
5618 shouldRetry = IsElementClickable(iWebElement);
5619 }
5620 else
5621 {
5622 BrowserHelper.ExecuteJavaScriptObj("arguments[0].style.display='none';", browser, overlay);
5623 }
5624 }
5625 }
5626 }
5627 }
5628 catch (NoSuchElementException)
5629 {
5630 // No overlay found, proceed
5631 }
5632 }
5633 }
5634 else if (exception is ElementNotVisibleException)
5635 {
5636 // Step 2: Scroll element into view (replaces JavaScriptMoveTo)
5637 ElementHelper.MoveTo(gPalElement, currentUOW.CurrentSelector.OffsetX, currentUOW.CurrentSelector.OffsetY, browser);
5638
5639 // Step 3: Wait for element to be clickable (visible and enabled)
5640 // Step 4: Adjust for sticky elements (e.g., headers)
5641 if (false == IsElementClickable(iWebElement))
5642 browser.ExecuteJavaScriptStr("window.scrollBy(0, -100, behavior: 'instant');"); // Scroll up slightly if needed
5643
5644 shouldRetry = IsElementClickable(iWebElement);
5645 }
5646 }
5647 catch (GPALException)
5648 {
5649 throw;
5650 }
5651 catch (Exception ex)
5652 {
5653 // Log recovery failure
5654 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Recovery failed", iWebElement, GPALObjectType.Other, ex);
5655 shouldRetry = false;
5656 }
5657
5658 return shouldRetry && retries > 0;
5659 }
5665 internal static bool IsElementClickable(IWebElement webElement)
5666 {
5667 return webElement.Displayed && webElement.Enabled;
5668 }
5669 internal static bool IsElementClickable(GPALElement element)
5670 {
5671 return element.Displayed && element.Enabled;
5672 }
5679 public static bool IsElementEditable(GPALElement elem)
5680 {
5681 try
5682 {
5683 // Get the tag name and type
5684 string tag = elem?.WebElement?.TagName.ToUpperInvariant() ?? elem?.TagName.ToUpperInvariant();
5685 string type = string.Empty;
5686 if (tag == "INPUT")
5687 {
5688 type = elem.GetAttribute("type")?.ToLower();
5689 }
5690
5691 // Check CSS cursor property
5692 string cursor = elem?.GetCssValue("cursor")?.ToLower();
5693 if (cursor == "text")
5694 return true;
5695
5696 // Check tag/type/contentEditable
5697 bool retval = (tag == "INPUT" &&
5698 (type == "text" || type == "email" || type == "password" || type == "search"))
5699 || tag == "TEXTAREA"
5700 || (tag == "DIV" && elem.GetAttribute("contenteditable")?.ToLower() == "true");
5701
5702 // Optional: check for editable classes
5703 if (!retval)
5704 {
5705 string classAttr = elem?.GetAttribute("class") ?? "";
5706 if (null != classAttr)
5707 retval = classAttr.Split(' ').Contains("Editable");
5708 }
5709
5710 return retval;
5711 }
5712 catch
5713 {
5714 // Fail-forward: if anything throws, consider editable as the developer thinks it is
5715 return true;
5716 }
5717 }
5733 public static void SendString(Browser browser, string textToSend)
5734 {
5735 int delayMs = GPAL.TypingDelay;
5736
5737 if (true == browser.BrowserSettings.UseHardware || true == GPAL.GPALSettings.UseHardware)
5738 HardwareHelper.SendString(textToSend, delayMs);
5739 else if (true == browser.BrowserSettings.UseOttoMagic)
5740 browser.MagicHelper.SendString(textToSend, delayMs);
5741 else if (true == browser.BrowserSettings.UsePuppeteer)
5742 browser.PuppeteerClient.SendString(textToSend, delayMs);
5743 else
5744 {
5745 foreach (SelectorSettings.SelectorPathEntry selectorPathEntry in browser.CurrentUOW.CurrentSelector?.SelectorPaths)
5746 if (null != selectorPathEntry.WebSelectorFoundResults)
5747 foreach (GPALElement webElement in selectorPathEntry.WebSelectorFoundResults)
5748 if (InteractionType.JavaScript == browser.CurrentUOW.CurrentSelector?.InteractionType || true == GPAL.GPALSettings.UseJavaScript)
5749 {
5750 if (0 == delayMs)
5751 BrowserHelper.ExecuteJavaScriptObj(@"
5752 function sendStringBlindly(text) {
5753 for (let char of text) {
5754 document.dispatchEvent(new KeyboardEvent('keydown', {{
5755 key: char,
5756 bubbles: true
5757 }}));
5758 document.dispatchEvent(new KeyboardEvent('keyup', {{
5759 key: char,
5760 bubbles: true
5761 }}));
5762 }
5763 }" +
5764 $"sendStringBlindly({textToSend});", browser);
5765 else
5766 {
5767 bool first = true;
5768 foreach (char c in textToSend)
5769 {
5770 if (false == first)
5771 Thread.Sleep(HardwareHelper.GetTypingDelay(delayMs));
5772 BrowserHelper.ExecuteJavaScriptObj(@"
5773 var char = arguments[0];
5774 document.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true }));
5775 document.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true }));",
5776 browser, c.ToString());
5777 first = false;
5778 }
5779 }
5780 }
5781 else // selenium - use the first element we have found or the body if no selector was set
5782 {
5783 IWebElement body = browser.BrowserSettings.BrowserDriver.FindElement(By.TagName("body"));
5784 // Use Actions to simulate typing each character with key down and key up
5785 Actions actions = new Actions(browser.BrowserSettings.BrowserDriver);
5786 bool first = true;
5787 foreach (char c in textToSend)
5788 {
5789 if (false == first && 0 < delayMs)
5790 actions.Pause(TimeSpan.FromMilliseconds(HardwareHelper.GetTypingDelay(delayMs)));
5791 actions
5792 .KeyDown(body, c.ToString())
5793 .KeyUp(body, c.ToString());
5794 first = false;
5795 }
5796 actions.Build().Perform();
5797 }
5798
5799 }
5800 }
5808 public static void SendKey(Browser browser, byte VKCode)
5809 {
5810 if (true == browser.BrowserSettings.UseHardware || true == GPAL.GPALSettings.UseHardware)
5811 {
5812 if (GPAL.VK_RETURN == VKCode)
5813 {
5814 var matched = browser.CurrentUOW.CurrentSelector?.WebSelectorMatchedResults?.FirstOrDefault();
5815 if (false == ElementHelper.IsElementEditable(matched))
5816 {
5817 matched.Click();
5818 return;
5819 }
5820 }
5821
5822 HardwareHelper.SendKey(VKCode);
5823 }
5824 else if (true == browser.BrowserSettings.UseOttoMagic)
5825 browser.MagicHelper.SendKey(VKCode);
5826 else if (true == browser.BrowserSettings.UsePuppeteer)
5827 browser.PuppeteerClient.SendKey(VKCode).Execute();
5828 else
5829 {
5830 {
5831 foreach (SelectorSettings.SelectorPathEntry selectorPathEntry in browser.CurrentUOW.CurrentSelector?.SelectorPaths)
5832 if (null != selectorPathEntry.WebSelectorFoundResults)
5833 foreach (GPALElement webElement in selectorPathEntry.WebSelectorFoundResults)
5834 if (InteractionType.JavaScript == browser.CurrentUOW.CurrentSelector?.InteractionType)
5835 ElementHelper.JavaScriptSendKey(browser, webElement, VKCode);
5836 else
5837 ElementHelper.SeleniumSendKeys(browser, webElement, VKCode);
5838 }
5839 }
5840 }
5841 public static string GetEnumNames<T>(T value) where T : struct, Enum
5842 {
5843 if (!typeof(T).IsDefined(typeof(FlagsAttribute), false))
5844 return value.ToString(); // or throw, or return ""
5845
5846 var flags = Enum.GetValues(typeof(T))
5847 .Cast<T>()
5848 .Where(flag => !Equals(flag, default(T)) && value.HasFlag(flag))
5849 .Select(flag => flag.ToString());
5850
5851 string retVal = string.Join(", ", flags);
5852
5853 return false == string.IsNullOrEmpty(retVal) ? retVal : "NONE";
5854 }
5862 public static string GenerateCssSelector(IBrowser browser, IWebElement webElement)
5863 {
5864 return BrowserHelper.ExecuteJavaScript(browser, @"
5865 function getCssPath(el) {
5866 if (!(el instanceof Element))
5867 return null;
5868
5869 var path = [];
5870
5871 while (el && el.nodeType === Node.ELEMENT_NODE) {
5872
5873 if (el.id) {
5874 path.unshift('#' + CSS.escape(el.id));
5875 break;
5876 }
5877
5878 var selector = el.nodeName.toLowerCase();
5879
5880 var sibling = el;
5881 var nth = 1;
5882
5883 while ((sibling = sibling.previousElementSibling) != null) {
5884 if (sibling.nodeName === el.nodeName)
5885 nth++;
5886 }
5887
5888 selector += ':nth-of-type(' + nth + ')';
5889
5890 path.unshift(selector);
5891
5892 el = el.parentElement;
5893 }
5894
5895 return path.join(' > ');
5896 }
5897 return getCssPath(arguments[0]);
5898 ", webElement);
5899 }
5900 }
5901}
5902
static string ExecuteJavaScript(IBrowser browser, string executeMe, params object[] args)
Runs a script and hands back what it produced. Selenium's executor is a function body,...
Browser object that contains the fluent methods to create your Browser workflow. Instatiated using G...
Definition Browser.cs:68
Pseudo element used in Applications and Browser workflows for image matching and unified automation....
dynamic WebElement
The wrapped Selenium IWebElement or internal dynamic element.
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...
Definition GPAL.cs:2406
Settings for the current selector. Use this only for debugging.