@xylentis/testgen 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,941 @@
1
+ /* @testgen-package */
2
+ // Generated by testgen: rewritten every time tests are generated, do not edit.
3
+ // testgen (@xylentis/testgen), Copyright (C) 2026 Xylentis. Licensed under AGPL-3.0-only with an additional
4
+ // permission: as part of tests generated by testgen, this file may be used, modified and distributed under
5
+ // terms of your choice (see NOTICE in @xylentis/testgen).
6
+
7
+ import java.net.MalformedURLException;
8
+ import java.net.URI;
9
+ import java.nio.file.Path;
10
+ import java.time.Duration;
11
+ import java.util.ArrayList;
12
+ import java.util.LinkedHashMap;
13
+ import java.util.List;
14
+ import java.util.Map;
15
+ import java.util.Set;
16
+ import java.util.TreeMap;
17
+ import java.util.function.Consumer;
18
+ import java.util.function.Function;
19
+ import java.util.function.Predicate;
20
+ import java.util.regex.Matcher;
21
+ import java.util.regex.Pattern;
22
+ import org.openqa.selenium.Alert;
23
+ import org.openqa.selenium.Capabilities;
24
+ import org.openqa.selenium.Dimension;
25
+ import org.openqa.selenium.ElementNotInteractableException;
26
+ import org.openqa.selenium.InvalidElementStateException;
27
+ import org.openqa.selenium.JavascriptException;
28
+ import org.openqa.selenium.JavascriptExecutor;
29
+ import org.openqa.selenium.Keys;
30
+ import org.openqa.selenium.NoSuchElementException;
31
+ import org.openqa.selenium.NoSuchFrameException;
32
+ import org.openqa.selenium.NoSuchWindowException;
33
+ import org.openqa.selenium.Rectangle;
34
+ import org.openqa.selenium.StaleElementReferenceException;
35
+ import org.openqa.selenium.TimeoutException;
36
+ import org.openqa.selenium.WebDriver;
37
+ import org.openqa.selenium.WebDriverException;
38
+ import org.openqa.selenium.WebElement;
39
+ import org.openqa.selenium.WindowType;
40
+ import org.openqa.selenium.chrome.ChromeDriver;
41
+ import org.openqa.selenium.chrome.ChromeOptions;
42
+ import org.openqa.selenium.edge.EdgeDriver;
43
+ import org.openqa.selenium.edge.EdgeOptions;
44
+ import org.openqa.selenium.firefox.FirefoxDriver;
45
+ import org.openqa.selenium.firefox.FirefoxOptions;
46
+ import org.openqa.selenium.interactions.Actions;
47
+ import org.openqa.selenium.remote.LocalFileDetector;
48
+ import org.openqa.selenium.remote.RemoteWebDriver;
49
+ import org.openqa.selenium.safari.SafariDriver;
50
+ import org.openqa.selenium.safari.SafariOptions;
51
+ import org.openqa.selenium.support.ui.ExpectedConditions;
52
+ import org.openqa.selenium.support.ui.Select;
53
+ import org.openqa.selenium.support.ui.WebDriverWait;
54
+
55
+ /**
56
+ * Playwright-style locators for Selenium WebDriver, e.g. {@code page.getByRole("button", "Sign in").click()},
57
+ * with Playwright-like auto-waiting. Locators are resolved inside the page by the testgen engine
58
+ * ({@link #ENGINE}), so getByRole/getByText/getByLabel find the elements the Playwright recorder pointed at.
59
+ *
60
+ * <p>Environment variables: HEADED=1 shows the browser, SELENIUM_REMOTE_URL runs on a Selenium Grid,
61
+ * TESTGEN_TIMEOUT sets the auto-wait timeout in milliseconds (default 10000).
62
+ */
63
+ public final class TestgenSupport {
64
+ private TestgenSupport() {}
65
+
66
+ static final Duration DEFAULT_TIMEOUT = Duration.ofMillis(Long.parseLong(env("TESTGEN_TIMEOUT", "10000")));
67
+ private static final boolean MAC = System.getProperty("os.name", "").toLowerCase().contains("mac");
68
+
69
+ private static String env(String name, String fallback) {
70
+ String value = System.getenv(name);
71
+ return value == null || value.isEmpty() ? fallback : value;
72
+ }
73
+
74
+ /** Starts "chrome", "edge", "firefox" or "safari", locally or on SELENIUM_REMOTE_URL; headless unless HEADED is set. */
75
+ public static WebDriver createDriver(String browser, String... arguments) {
76
+ boolean headed = !env("HEADED", "").isEmpty();
77
+ Capabilities options;
78
+ switch (browser) {
79
+ case "firefox" -> {
80
+ FirefoxOptions firefox = new FirefoxOptions().addArguments(arguments);
81
+ options = headed ? firefox : firefox.addArguments("-headless");
82
+ }
83
+ case "edge" -> {
84
+ EdgeOptions edge = new EdgeOptions();
85
+ edge.addArguments(arguments);
86
+ if (!headed)
87
+ edge.addArguments("--headless=new");
88
+ options = edge;
89
+ }
90
+ case "safari" -> options = new SafariOptions();
91
+ default -> {
92
+ ChromeOptions chrome = new ChromeOptions();
93
+ chrome.addArguments(arguments);
94
+ if (!headed)
95
+ chrome.addArguments("--headless=new");
96
+ options = chrome;
97
+ }
98
+ }
99
+ String remote = env("SELENIUM_REMOTE_URL", "");
100
+ if (!remote.isEmpty()) {
101
+ try {
102
+ RemoteWebDriver driver = new RemoteWebDriver(URI.create(remote).toURL(), options);
103
+ driver.setFileDetector(new LocalFileDetector());
104
+ return driver;
105
+ } catch (MalformedURLException e) {
106
+ throw new IllegalArgumentException("SELENIUM_REMOTE_URL is not a valid URL: " + remote, e);
107
+ }
108
+ }
109
+ return switch (browser) {
110
+ case "firefox" -> new FirefoxDriver((FirefoxOptions) options);
111
+ case "edge" -> new EdgeDriver((EdgeOptions) options);
112
+ case "safari" -> new SafariDriver((SafariOptions) options);
113
+ default -> new ChromeDriver((ChromeOptions) options);
114
+ };
115
+ }
116
+
117
+ /** Playwright viewports are inner sizes; WebDriver sizes the whole window. */
118
+ public static void setViewportSize(WebDriver driver, int width, int height) {
119
+ driver.manage().window().setSize(new Dimension(width, height));
120
+ List<?> inner = (List<?>) ((JavascriptExecutor) driver).executeScript("return [window.innerWidth, window.innerHeight];");
121
+ int innerWidth = ((Number) inner.get(0)).intValue();
122
+ int innerHeight = ((Number) inner.get(1)).intValue();
123
+ if (innerWidth != width || innerHeight != height)
124
+ driver.manage().window().setSize(new Dimension(2 * width - innerWidth, 2 * height - innerHeight));
125
+ }
126
+
127
+ /** A chain of Playwright-style locator steps; nothing is looked up until an action or assertion runs. */
128
+ public static class Locator {
129
+ private final Page page;
130
+ final List<Map<String, Object>> steps;
131
+ private final String description;
132
+
133
+ Locator(Page page, List<Map<String, Object>> steps, String description) {
134
+ this.page = page;
135
+ this.steps = steps;
136
+ this.description = description;
137
+ }
138
+
139
+ Page page() {
140
+ return this instanceof Page self ? self : page;
141
+ }
142
+
143
+ @Override
144
+ public String toString() {
145
+ return description;
146
+ }
147
+
148
+ private Locator with(Map<String, Object> step, String call) {
149
+ List<Map<String, Object>> next = new ArrayList<>(steps);
150
+ next.add(step);
151
+ return new Locator(page(), next, description + "." + call);
152
+ }
153
+
154
+ private static Map<String, Object> step(String type) {
155
+ Map<String, Object> step = new LinkedHashMap<>();
156
+ step.put("t", type);
157
+ return step;
158
+ }
159
+
160
+ public Locator getByRole(String role) {
161
+ return role(role, Map.of(), quote(role));
162
+ }
163
+
164
+ public Locator getByRole(String role, String name) {
165
+ return role(role, Map.of("name", name), quote(role) + ", " + quote(name));
166
+ }
167
+
168
+ /** Options: name (String or Pattern), exact, checked, disabled, expanded, includeHidden, level, pressed, selected. */
169
+ public Locator getByRole(String role, Map<String, ?> options) {
170
+ return role(role, options, quote(role) + ", " + format(options));
171
+ }
172
+
173
+ private Locator role(String role, Map<String, ?> options, String arguments) {
174
+ Map<String, Object> step = step("role");
175
+ step.put("role", role);
176
+ Map<String, Object> attrs = new TreeMap<>();
177
+ for (Map.Entry<String, ?> option : options.entrySet()) {
178
+ switch (option.getKey()) {
179
+ case "name" -> step.put("name", matcher(option.getValue()));
180
+ case "exact" -> step.put("exact", option.getValue());
181
+ case "checked", "disabled", "expanded", "includeHidden", "level", "pressed", "selected" -> attrs.put(option.getKey(), option.getValue());
182
+ default -> throw new IllegalArgumentException("Unknown getByRole option: " + option.getKey());
183
+ }
184
+ }
185
+ if (!attrs.isEmpty())
186
+ step.put("attrs", attrs);
187
+ return with(step, "getByRole(" + arguments + ")");
188
+ }
189
+
190
+ public Locator getByText(String text) {
191
+ return byText("text", "getByText", text, false);
192
+ }
193
+
194
+ public Locator getByText(String text, boolean exact) {
195
+ return byText("text", "getByText", text, exact);
196
+ }
197
+
198
+ public Locator getByText(Pattern text) {
199
+ return byText("text", "getByText", text, false);
200
+ }
201
+
202
+ public Locator getByLabel(String text) {
203
+ return byText("label", "getByLabel", text, false);
204
+ }
205
+
206
+ public Locator getByLabel(String text, boolean exact) {
207
+ return byText("label", "getByLabel", text, exact);
208
+ }
209
+
210
+ public Locator getByLabel(Pattern text) {
211
+ return byText("label", "getByLabel", text, false);
212
+ }
213
+
214
+ public Locator getByPlaceholder(String text) {
215
+ return byText("placeholder", "getByPlaceholder", text, false);
216
+ }
217
+
218
+ public Locator getByPlaceholder(String text, boolean exact) {
219
+ return byText("placeholder", "getByPlaceholder", text, exact);
220
+ }
221
+
222
+ public Locator getByPlaceholder(Pattern text) {
223
+ return byText("placeholder", "getByPlaceholder", text, false);
224
+ }
225
+
226
+ public Locator getByAltText(String text) {
227
+ return byText("alt", "getByAltText", text, false);
228
+ }
229
+
230
+ public Locator getByAltText(String text, boolean exact) {
231
+ return byText("alt", "getByAltText", text, exact);
232
+ }
233
+
234
+ public Locator getByAltText(Pattern text) {
235
+ return byText("alt", "getByAltText", text, false);
236
+ }
237
+
238
+ public Locator getByTitle(String text) {
239
+ return byText("title", "getByTitle", text, false);
240
+ }
241
+
242
+ public Locator getByTitle(String text, boolean exact) {
243
+ return byText("title", "getByTitle", text, exact);
244
+ }
245
+
246
+ public Locator getByTitle(Pattern text) {
247
+ return byText("title", "getByTitle", text, false);
248
+ }
249
+
250
+ private Locator byText(String type, String method, Object text, boolean exact) {
251
+ Map<String, Object> step = step(type);
252
+ step.put("v", matcher(text));
253
+ step.put("exact", exact);
254
+ return with(step, method + "(" + format(text) + (exact ? ", true" : "") + ")");
255
+ }
256
+
257
+ public Locator getByTestId(String testId) {
258
+ return byTestId(testId);
259
+ }
260
+
261
+ public Locator getByTestId(Pattern testId) {
262
+ return byTestId(testId);
263
+ }
264
+
265
+ private Locator byTestId(Object testId) {
266
+ Map<String, Object> step = step("testid");
267
+ step.put("attr", page().testIdAttribute);
268
+ step.put("v", matcher(testId));
269
+ return with(step, "getByTestId(" + format(testId) + ")");
270
+ }
271
+
272
+ /** CSS by default; also "xpath=...", "//...", "css=..." and Playwright's internal:has-text selectors. */
273
+ public Locator locator(String selector) {
274
+ return with(selectorStep(selector), "locator(" + quote(selector) + ")");
275
+ }
276
+
277
+ public Locator locator(Locator inner) {
278
+ List<Map<String, Object>> next = new ArrayList<>(steps);
279
+ next.addAll(inner.steps);
280
+ return new Locator(page(), next, description + ".locator(" + inner + ")");
281
+ }
282
+
283
+ public Locator frameLocator(String selector) {
284
+ return locator(selector).contentFrame();
285
+ }
286
+
287
+ public Locator contentFrame() {
288
+ return with(step("frame"), "contentFrame()");
289
+ }
290
+
291
+ public Locator filterHasText(String text) {
292
+ return hasText(text, false, "filterHasText");
293
+ }
294
+
295
+ public Locator filterHasText(Pattern text) {
296
+ return hasText(text, false, "filterHasText");
297
+ }
298
+
299
+ public Locator filterHasNotText(String text) {
300
+ return hasText(text, true, "filterHasNotText");
301
+ }
302
+
303
+ public Locator filterHasNotText(Pattern text) {
304
+ return hasText(text, true, "filterHasNotText");
305
+ }
306
+
307
+ private Locator hasText(Object text, boolean not, String method) {
308
+ Map<String, Object> step = step("hasText");
309
+ step.put("v", matcher(text));
310
+ step.put("not", not);
311
+ return with(step, method + "(" + format(text) + ")");
312
+ }
313
+
314
+ public Locator filterHas(Locator inner) {
315
+ return has(inner, false, "filterHas");
316
+ }
317
+
318
+ public Locator filterHasNot(Locator inner) {
319
+ return has(inner, true, "filterHasNot");
320
+ }
321
+
322
+ private Locator has(Locator inner, boolean not, String method) {
323
+ Map<String, Object> step = step("has");
324
+ step.put("steps", inner.steps);
325
+ step.put("not", not);
326
+ return with(step, method + "(" + inner + ")");
327
+ }
328
+
329
+ public Locator filterVisible(boolean visible) {
330
+ Map<String, Object> step = step("visible");
331
+ step.put("v", visible);
332
+ return with(step, "filterVisible(" + visible + ")");
333
+ }
334
+
335
+ public Locator nth(int index) {
336
+ Map<String, Object> step = step("nth");
337
+ step.put("v", index);
338
+ return with(step, "nth(" + index + ")");
339
+ }
340
+
341
+ public Locator first() {
342
+ return nth(0);
343
+ }
344
+
345
+ public Locator last() {
346
+ return nth(-1);
347
+ }
348
+
349
+ public Locator and(Locator other) {
350
+ Map<String, Object> step = step("and");
351
+ step.put("steps", other.steps);
352
+ return with(step, "and(" + other + ")");
353
+ }
354
+
355
+ public Locator or(Locator other) {
356
+ Map<String, Object> step = step("or");
357
+ step.put("steps", other.steps);
358
+ return with(step, "or(" + other + ")");
359
+ }
360
+
361
+ /** Elements currently matching the locator (no waiting). */
362
+ public List<WebElement> elements() {
363
+ Page page = page();
364
+ page.activate();
365
+ page.driver.switchTo().defaultContent();
366
+ List<Map<String, Object>> segment = new ArrayList<>();
367
+ for (Map<String, Object> step : steps) {
368
+ if (!"frame".equals(step.get("t"))) {
369
+ segment.add(step);
370
+ continue;
371
+ }
372
+ List<WebElement> frames = page.resolve(segment);
373
+ if (frames.isEmpty())
374
+ return List.of();
375
+ page.driver.switchTo().frame(frames.get(0));
376
+ segment = new ArrayList<>();
377
+ }
378
+ return page.resolve(segment);
379
+ }
380
+
381
+ public int count() {
382
+ return elements().size();
383
+ }
384
+
385
+ /** Waits for the first matching element to be attached and returns it. */
386
+ public WebElement element() {
387
+ return query("element", element -> element, false, false);
388
+ }
389
+
390
+ // Waits for the first matching element to be ready, then runs `action` on it.
391
+ private <T> T query(String name, Function<WebElement, T> action, boolean visible, boolean enabled) {
392
+ Page page = page();
393
+ long deadline = System.nanoTime() + page.timeout.toNanos();
394
+ String reason;
395
+ while (true) {
396
+ try {
397
+ List<WebElement> found = elements();
398
+ if (found.isEmpty()) {
399
+ reason = "no element matches";
400
+ } else if (visible && !found.get(0).isDisplayed()) {
401
+ reason = "element is not visible";
402
+ } else if (enabled && !found.get(0).isEnabled()) {
403
+ reason = "element is not enabled";
404
+ } else {
405
+ T result = action.apply(found.get(0));
406
+ page.handleDialogs();
407
+ return result;
408
+ }
409
+ } catch (WebDriverException e) {
410
+ if (!isRetryable(e))
411
+ throw e;
412
+ reason = firstLine(e.getMessage());
413
+ }
414
+ if (System.nanoTime() > deadline)
415
+ throw new TimeoutException(name + ": timeout " + page.timeout.toMillis() + "ms exceeded waiting for " + this + " (" + reason + ")");
416
+ sleep(100);
417
+ }
418
+ }
419
+
420
+ private void act(String name, Consumer<WebElement> action, boolean visible, boolean enabled) {
421
+ query(name, element -> {
422
+ action.accept(element);
423
+ return null;
424
+ }, visible, enabled);
425
+ }
426
+
427
+ public void click() {
428
+ act("click", WebElement::click, true, true);
429
+ }
430
+
431
+ public void click(ClickOptions options) {
432
+ act("click", element -> options.perform(page().driver, element, true), true, true);
433
+ }
434
+
435
+ public void dblclick() {
436
+ act("dblclick", element -> new Actions(page().driver).doubleClick(element).perform(), true, true);
437
+ }
438
+
439
+ public void hover() {
440
+ act("hover", element -> new Actions(page().driver).moveToElement(element).perform(), true, false);
441
+ }
442
+
443
+ public void hover(ClickOptions options) {
444
+ act("hover", element -> options.perform(page().driver, element, false), true, false);
445
+ }
446
+
447
+ public void fill(String value) {
448
+ act("fill", element -> {
449
+ element.clear();
450
+ if (!value.isEmpty())
451
+ element.sendKeys(value);
452
+ }, true, true);
453
+ }
454
+
455
+ public void press(String key) {
456
+ act("press", element -> element.sendKeys(keys(key)), true, false);
457
+ }
458
+
459
+ public void check() {
460
+ setChecked(true);
461
+ }
462
+
463
+ public void uncheck() {
464
+ setChecked(false);
465
+ }
466
+
467
+ private void setChecked(boolean checked) {
468
+ act(checked ? "check" : "uncheck", element -> {
469
+ if (isChecked(element) != checked)
470
+ element.click();
471
+ }, true, true);
472
+ }
473
+
474
+ public void selectOption(String... values) {
475
+ act("selectOption", element -> {
476
+ Select select = new Select(element);
477
+ if (select.isMultiple())
478
+ select.deselectAll();
479
+ for (String value : values) {
480
+ try {
481
+ select.selectByValue(value);
482
+ } catch (NoSuchElementException e) {
483
+ select.selectByVisibleText(value);
484
+ }
485
+ }
486
+ }, true, true);
487
+ }
488
+
489
+ public void setInputFiles(String... files) {
490
+ List<String> paths = new ArrayList<>();
491
+ for (String file : files)
492
+ paths.add(Path.of(file).toAbsolutePath().toString());
493
+ act("setInputFiles", element -> element.sendKeys(String.join("\n", paths)), false, false);
494
+ }
495
+
496
+ public void shouldBeVisible() {
497
+ expect("shouldBeVisible", null, WebElement::isDisplayed, element -> element.isDisplayed() ? "visible" : "hidden");
498
+ }
499
+
500
+ public void shouldHaveText(String expected) {
501
+ expect("shouldHaveText", expected, element -> textOf(element).equals(normalize(expected)), TestgenSupport::textOf);
502
+ }
503
+
504
+ public void shouldContainText(String expected) {
505
+ expect("shouldContainText", expected, element -> textOf(element).contains(normalize(expected)), TestgenSupport::textOf);
506
+ }
507
+
508
+ public void shouldHaveValue(String expected) {
509
+ expect("shouldHaveValue", expected, element -> expected.equals(element.getDomProperty("value")), element -> element.getDomProperty("value"));
510
+ }
511
+
512
+ public void shouldBeEmpty() {
513
+ expect("shouldBeEmpty", null, element -> valueOrText(element).isEmpty(), TestgenSupport::valueOrText);
514
+ }
515
+
516
+ public void shouldBeChecked() {
517
+ expect("shouldBeChecked", null, TestgenSupport::isChecked, element -> isChecked(element) ? "checked" : "unchecked");
518
+ }
519
+
520
+ public void shouldNotBeChecked() {
521
+ expect("shouldNotBeChecked", null, element -> !isChecked(element), element -> isChecked(element) ? "checked" : "unchecked");
522
+ }
523
+
524
+ // Polls until `check` passes for the first matching element.
525
+ private void expect(String name, String expected, Predicate<WebElement> check, Function<WebElement, String> actual) {
526
+ Page page = page();
527
+ long deadline = System.nanoTime() + page.timeout.toNanos();
528
+ String received = "<element not found>";
529
+ while (true) {
530
+ try {
531
+ List<WebElement> found = elements();
532
+ if (!found.isEmpty()) {
533
+ if (check.test(found.get(0)))
534
+ return;
535
+ received = actual.apply(found.get(0));
536
+ }
537
+ } catch (WebDriverException e) {
538
+ if (!isRetryable(e))
539
+ throw e;
540
+ received = firstLine(e.getMessage());
541
+ }
542
+ if (System.nanoTime() > deadline)
543
+ throw new AssertionError(this + "." + name + "(" + (expected == null ? "" : quote(expected)) + ") failed after " + page.timeout.toMillis() + "ms\nReceived: " + received);
544
+ sleep(100);
545
+ }
546
+ }
547
+ }
548
+
549
+ /** One browser window or tab, like Playwright's Page; actions switch WebDriver to it automatically. */
550
+ public static final class Page extends Locator {
551
+ final WebDriver driver;
552
+ final String handle;
553
+ String testIdAttribute = "data-testid";
554
+ Duration timeout = DEFAULT_TIMEOUT;
555
+ private final List<Consumer<Dialog>> dialogHandlers = new ArrayList<>();
556
+
557
+ public Page(WebDriver driver) {
558
+ this(driver, driver.getWindowHandle());
559
+ }
560
+
561
+ private Page(WebDriver driver, String handle) {
562
+ super(null, List.of(), "page");
563
+ this.driver = driver;
564
+ this.handle = handle;
565
+ }
566
+
567
+ public Page setTestIdAttribute(String attribute) {
568
+ testIdAttribute = attribute;
569
+ return this;
570
+ }
571
+
572
+ public Page setDefaultTimeout(Duration timeout) {
573
+ this.timeout = timeout;
574
+ return this;
575
+ }
576
+
577
+ public void navigate(String url) {
578
+ activate();
579
+ driver.get(url);
580
+ }
581
+
582
+ public void close() {
583
+ activate();
584
+ driver.close();
585
+ }
586
+
587
+ /** Opens a new tab. */
588
+ public Page newPage() {
589
+ driver.switchTo().newWindow(WindowType.TAB);
590
+ return sibling(driver.getWindowHandle());
591
+ }
592
+
593
+ /** Runs `action` and returns the window it opens. */
594
+ public Page waitForPopup(Runnable action) {
595
+ activate();
596
+ Set<String> before = driver.getWindowHandles();
597
+ action.run();
598
+ long deadline = System.nanoTime() + timeout.toNanos();
599
+ while (true) {
600
+ for (String candidate : driver.getWindowHandles()) {
601
+ if (!before.contains(candidate))
602
+ return sibling(candidate);
603
+ }
604
+ if (System.nanoTime() > deadline)
605
+ throw new TimeoutException("waitForPopup: timeout " + timeout.toMillis() + "ms exceeded");
606
+ sleep(100);
607
+ }
608
+ }
609
+
610
+ /** The handler runs for the next alert/confirm/prompt opened by an action on this page. */
611
+ public void onceDialog(Consumer<Dialog> handler) {
612
+ dialogHandlers.add(handler);
613
+ }
614
+
615
+ void handleDialogs() {
616
+ if (dialogHandlers.isEmpty())
617
+ return;
618
+ Alert alert;
619
+ try {
620
+ alert = new WebDriverWait(driver, Duration.ofSeconds(2)).until(ExpectedConditions.alertIsPresent());
621
+ } catch (TimeoutException e) {
622
+ return;
623
+ }
624
+ dialogHandlers.remove(0).accept(new Dialog(alert));
625
+ }
626
+
627
+ void activate() {
628
+ String current;
629
+ try {
630
+ current = driver.getWindowHandle();
631
+ } catch (NoSuchWindowException e) {
632
+ current = null;
633
+ }
634
+ if (!handle.equals(current))
635
+ driver.switchTo().window(handle);
636
+ }
637
+
638
+ List<WebElement> resolve(List<Map<String, Object>> steps) {
639
+ Object result = ((JavascriptExecutor) driver).executeScript("return (" + ENGINE + ").apply(null, arguments);", json(steps));
640
+ List<WebElement> elements = new ArrayList<>();
641
+ if (result instanceof List<?> list) {
642
+ for (Object item : list)
643
+ elements.add((WebElement) item);
644
+ }
645
+ return elements;
646
+ }
647
+
648
+ private Page sibling(String handle) {
649
+ Page page = new Page(driver, handle);
650
+ page.testIdAttribute = testIdAttribute;
651
+ page.timeout = timeout;
652
+ return page;
653
+ }
654
+ }
655
+
656
+ public static final class Dialog {
657
+ private final Alert alert;
658
+ private final String message;
659
+
660
+ Dialog(Alert alert) {
661
+ this.alert = alert;
662
+ this.message = alert.getText();
663
+ }
664
+
665
+ public String message() {
666
+ return message;
667
+ }
668
+
669
+ public void dismiss() {
670
+ alert.dismiss();
671
+ }
672
+
673
+ public void accept() {
674
+ alert.accept();
675
+ }
676
+
677
+ public void accept(String promptText) {
678
+ alert.sendKeys(promptText);
679
+ alert.accept();
680
+ }
681
+ }
682
+
683
+ /** Mouse options of a recorded click: button, modifiers, click count, position from the top-left corner. */
684
+ public static final class ClickOptions {
685
+ private String button = "left";
686
+ private int clickCount = 1;
687
+ private String[] modifiers = {};
688
+ private Double x;
689
+ private Double y;
690
+
691
+ public ClickOptions setButton(String button) {
692
+ this.button = button;
693
+ return this;
694
+ }
695
+
696
+ public ClickOptions setClickCount(int clickCount) {
697
+ this.clickCount = clickCount;
698
+ return this;
699
+ }
700
+
701
+ public ClickOptions setModifiers(String... modifiers) {
702
+ this.modifiers = modifiers;
703
+ return this;
704
+ }
705
+
706
+ public ClickOptions setPosition(double x, double y) {
707
+ this.x = x;
708
+ this.y = y;
709
+ return this;
710
+ }
711
+
712
+ void perform(WebDriver driver, WebElement element, boolean click) {
713
+ if ("middle".equals(button))
714
+ throw new UnsupportedOperationException("testgen: middle click is not supported on Selenium");
715
+ Actions actions = new Actions(driver);
716
+ for (String modifier : modifiers)
717
+ actions.keyDown(modifierKey(modifier));
718
+ if (x != null) {
719
+ // WebDriver offsets are relative to the element center.
720
+ Rectangle rect = element.getRect();
721
+ actions.moveToElement(element, (int) Math.round(x - rect.getWidth() / 2.0), (int) Math.round(y - rect.getHeight() / 2.0));
722
+ } else {
723
+ actions.moveToElement(element);
724
+ }
725
+ if (click && "right".equals(button))
726
+ actions.contextClick();
727
+ else if (click && clickCount == 2)
728
+ actions.doubleClick();
729
+ else if (click)
730
+ for (int i = 0; i < clickCount; i++)
731
+ actions.click();
732
+ for (int i = modifiers.length - 1; i >= 0; i--)
733
+ actions.keyUp(modifierKey(modifiers[i]));
734
+ actions.perform();
735
+ }
736
+ }
737
+
738
+ static boolean isChecked(WebElement element) {
739
+ String type = String.valueOf(element.getDomAttribute("type")).toLowerCase();
740
+ if ("input".equalsIgnoreCase(element.getTagName()) && (type.equals("checkbox") || type.equals("radio")))
741
+ return element.isSelected();
742
+ return "true".equals(element.getDomAttribute("aria-checked"));
743
+ }
744
+
745
+ static String textOf(WebElement element) {
746
+ return normalize(element.getDomProperty("textContent"));
747
+ }
748
+
749
+ static String valueOrText(WebElement element) {
750
+ String tag = element.getTagName().toLowerCase();
751
+ if (tag.equals("input") || tag.equals("textarea"))
752
+ return String.valueOf(element.getDomProperty("value"));
753
+ return textOf(element);
754
+ }
755
+
756
+ static String normalize(String text) {
757
+ return text == null ? "" : text.replace("​", "").trim().replaceAll("\\s+", " ");
758
+ }
759
+
760
+ static boolean isRetryable(WebDriverException e) {
761
+ if (e instanceof StaleElementReferenceException || e instanceof ElementNotInteractableException
762
+ || e instanceof NoSuchElementException || e instanceof NoSuchFrameException
763
+ || e instanceof InvalidElementStateException || e instanceof NoSuchWindowException)
764
+ return true;
765
+ // Scripts fail while the page navigates; real script errors carry a testgen or syntax message.
766
+ return e instanceof JavascriptException && !String.valueOf(e.getMessage()).matches("(?s).*(testgen:|SyntaxError|not a valid).*");
767
+ }
768
+
769
+ private static String firstLine(String message) {
770
+ return message == null ? "" : message.split("\n", 2)[0];
771
+ }
772
+
773
+ private static void sleep(long millis) {
774
+ try {
775
+ Thread.sleep(millis);
776
+ } catch (InterruptedException e) {
777
+ Thread.currentThread().interrupt();
778
+ throw new IllegalStateException(e);
779
+ }
780
+ }
781
+
782
+ // Text value for the engine: a string, or {source, flags} for a regular expression.
783
+ private static Object matcher(Object value) {
784
+ if (!(value instanceof Pattern pattern))
785
+ return String.valueOf(value);
786
+ String flags = ((pattern.flags() & Pattern.CASE_INSENSITIVE) != 0 ? "i" : "")
787
+ + ((pattern.flags() & Pattern.MULTILINE) != 0 ? "m" : "")
788
+ + ((pattern.flags() & Pattern.DOTALL) != 0 ? "s" : "");
789
+ Map<String, Object> regex = new LinkedHashMap<>();
790
+ regex.put("source", pattern.pattern());
791
+ regex.put("flags", flags);
792
+ return regex;
793
+ }
794
+
795
+ // Text, label, placeholder, ... values of internal:has-text selectors: "text"i, "text"s or /regex/flags.
796
+ private static Map<String, Object> selectorStep(String selector) {
797
+ Map<String, Object> step = new LinkedHashMap<>();
798
+ Matcher hasText = Pattern.compile("^internal:(has-text|has-not-text)=(.*)$", Pattern.DOTALL).matcher(selector);
799
+ if (selector.startsWith("xpath=")) {
800
+ step.put("t", "xpath");
801
+ step.put("v", selector.substring("xpath=".length()));
802
+ } else if (selector.startsWith("//") || selector.startsWith("..")) {
803
+ step.put("t", "xpath");
804
+ step.put("v", selector);
805
+ } else if (selector.startsWith("css=")) {
806
+ step.put("t", "css");
807
+ step.put("v", selector.substring("css=".length()));
808
+ } else if (hasText.matches()) {
809
+ String body = hasText.group(2);
810
+ Matcher regex = Pattern.compile("^/(.*)/([a-z]*)$", Pattern.DOTALL).matcher(body);
811
+ step.put("t", "hasText");
812
+ step.put("not", hasText.group(1).equals("has-not-text"));
813
+ if (regex.matches()) {
814
+ step.put("v", Map.of("source", regex.group(1), "flags", regex.group(2)));
815
+ } else {
816
+ boolean exact = !body.endsWith("\"i");
817
+ step.put("v", unquote(exact && body.endsWith("\"s") ? body.substring(0, body.length() - 1) : exact ? body : body.substring(0, body.length() - 1)));
818
+ step.put("exact", exact);
819
+ }
820
+ } else if (selector.startsWith("internal:") || selector.matches("^[a-zA-Z_0-9-]+=.*")) {
821
+ throw new IllegalArgumentException("testgen: selector not supported on Selenium: " + selector);
822
+ } else {
823
+ step.put("t", "css");
824
+ step.put("v", selector);
825
+ }
826
+ return step;
827
+ }
828
+
829
+ private static String unquote(String quoted) {
830
+ if (!quoted.startsWith("\"") || !quoted.endsWith("\"") || quoted.length() < 2)
831
+ return quoted;
832
+ StringBuilder result = new StringBuilder();
833
+ for (int i = 1; i < quoted.length() - 1; i++) {
834
+ char c = quoted.charAt(i);
835
+ if (c == '\\' && i + 1 < quoted.length() - 1) {
836
+ char next = quoted.charAt(++i);
837
+ result.append(switch (next) {
838
+ case 'n' -> '\n';
839
+ case 't' -> '\t';
840
+ case 'r' -> '\r';
841
+ default -> next;
842
+ });
843
+ } else {
844
+ result.append(c);
845
+ }
846
+ }
847
+ return result.toString();
848
+ }
849
+
850
+ private static final Map<String, CharSequence> NAMED_KEYS = Map.ofEntries(
851
+ Map.entry("Enter", Keys.ENTER), Map.entry("Tab", Keys.TAB), Map.entry("Escape", Keys.ESCAPE),
852
+ Map.entry("Backspace", Keys.BACK_SPACE), Map.entry("Delete", Keys.DELETE), Map.entry("Insert", Keys.INSERT),
853
+ Map.entry("ArrowUp", Keys.ARROW_UP), Map.entry("ArrowDown", Keys.ARROW_DOWN),
854
+ Map.entry("ArrowLeft", Keys.ARROW_LEFT), Map.entry("ArrowRight", Keys.ARROW_RIGHT),
855
+ Map.entry("Home", Keys.HOME), Map.entry("End", Keys.END), Map.entry("PageUp", Keys.PAGE_UP),
856
+ Map.entry("PageDown", Keys.PAGE_DOWN), Map.entry("Space", " "),
857
+ Map.entry("F1", Keys.F1), Map.entry("F2", Keys.F2), Map.entry("F3", Keys.F3), Map.entry("F4", Keys.F4),
858
+ Map.entry("F5", Keys.F5), Map.entry("F6", Keys.F6), Map.entry("F7", Keys.F7), Map.entry("F8", Keys.F8),
859
+ Map.entry("F9", Keys.F9), Map.entry("F10", Keys.F10), Map.entry("F11", Keys.F11), Map.entry("F12", Keys.F12));
860
+
861
+ static Keys modifierKey(String name) {
862
+ return switch (name) {
863
+ case "Alt" -> Keys.ALT;
864
+ case "Control" -> Keys.CONTROL;
865
+ case "Meta" -> Keys.META;
866
+ case "Shift" -> Keys.SHIFT;
867
+ case "ControlOrMeta" -> MAC ? Keys.META : Keys.CONTROL;
868
+ default -> throw new IllegalArgumentException("testgen: unknown modifier " + name);
869
+ };
870
+ }
871
+
872
+ /** Playwright key names ("Enter", "Shift+Tab", "ControlOrMeta+a") to WebDriver keys. */
873
+ static CharSequence keys(String shortcut) {
874
+ String[] parts = shortcut.split("\\+(?=.)");
875
+ List<CharSequence> chord = new ArrayList<>();
876
+ for (int i = 0; i < parts.length - 1; i++)
877
+ chord.add(modifierKey(parts[i]));
878
+ String key = parts[parts.length - 1];
879
+ chord.add(NAMED_KEYS.getOrDefault(key, key));
880
+ return chord.size() == 1 ? chord.get(0) : Keys.chord(chord);
881
+ }
882
+
883
+ static String quote(String text) {
884
+ return json(text);
885
+ }
886
+
887
+ private static String format(Object value) {
888
+ if (value instanceof Pattern pattern)
889
+ return "Pattern.compile(" + quote(pattern.pattern()) + ")";
890
+ if (value instanceof Map<?, ?> map) {
891
+ List<String> entries = new ArrayList<>();
892
+ for (Map.Entry<?, ?> entry : new TreeMap<>(map).entrySet())
893
+ entries.add(quote(String.valueOf(entry.getKey())) + ", " + format(entry.getValue()));
894
+ return "Map.of(" + String.join(", ", entries) + ")";
895
+ }
896
+ return value instanceof String text ? quote(text) : String.valueOf(value);
897
+ }
898
+
899
+ // Minimal JSON writer for engine steps (strings, numbers, booleans, lists and maps).
900
+ static String json(Object value) {
901
+ if (value == null)
902
+ return "null";
903
+ if (value instanceof Boolean || value instanceof Number)
904
+ return value.toString();
905
+ if (value instanceof Map<?, ?> map) {
906
+ List<String> entries = new ArrayList<>();
907
+ for (Map.Entry<?, ?> entry : map.entrySet())
908
+ entries.add(json(String.valueOf(entry.getKey())) + ":" + json(entry.getValue()));
909
+ return "{" + String.join(",", entries) + "}";
910
+ }
911
+ if (value instanceof List<?> list) {
912
+ List<String> items = new ArrayList<>();
913
+ for (Object item : list)
914
+ items.add(json(item));
915
+ return "[" + String.join(",", items) + "]";
916
+ }
917
+ String text = value.toString();
918
+ StringBuilder result = new StringBuilder("\"");
919
+ for (char c : text.toCharArray()) {
920
+ switch (c) {
921
+ case '"' -> result.append("\\\"");
922
+ case '\\' -> result.append("\\\\");
923
+ case '\n' -> result.append("\\n");
924
+ case '\r' -> result.append("\\r");
925
+ case '\t' -> result.append("\\t");
926
+ default -> {
927
+ if (c < 0x20)
928
+ result.append(String.format("\\u%04x", (int) c));
929
+ else
930
+ result.append(c);
931
+ }
932
+ }
933
+ }
934
+ return result.append('"').toString();
935
+ }
936
+
937
+ /** The testgen locator engine, run in the page through executeScript. */
938
+ static final String ENGINE = """
939
+ @testgen-engine
940
+ """;
941
+ }