@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,618 @@
1
+ // Generated by testgen: rewritten every time tests are generated, do not edit.
2
+ // testgen (@xylentis/testgen), Copyright (C) 2026 Xylentis. Licensed under AGPL-3.0-only with an additional
3
+ // permission: as part of tests generated by testgen, this file may be used, modified and distributed under
4
+ // terms of your choice (see NOTICE in @xylentis/testgen).
5
+ //
6
+ // The Playwright locator API (page.getByRole(), locator.click(), expect(locator).toHaveText(), ...)
7
+ // implemented on Selenium WebDriver, with Playwright-like auto-waiting. Locators are resolved inside
8
+ // the page by testgenResolve() (bottom of this file), so getByRole/getByText/getByLabel find the
9
+ // same elements the Playwright recorder pointed at.
10
+ //
11
+ // Environment variables: HEADED=1 shows the browser, SELENIUM_REMOTE_URL runs on a Selenium Grid,
12
+ // TESTGEN_TIMEOUT sets the auto-wait timeout in milliseconds (default 10000).
13
+ 'use strict';
14
+
15
+ const assert = require('node:assert');
16
+ const path = require('node:path');
17
+ const { Browser, Builder, Button, Key, Select, until } = require('selenium-webdriver');
18
+ const chrome = require('selenium-webdriver/chrome');
19
+ const edge = require('selenium-webdriver/edge');
20
+ const firefox = require('selenium-webdriver/firefox');
21
+ const remote = require('selenium-webdriver/remote');
22
+
23
+ const defaultTimeout = Number(process.env.TESTGEN_TIMEOUT || 10_000);
24
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
25
+
26
+ async function createDriver(browser = 'chrome', options = {}) {
27
+ const headed = !!process.env.HEADED;
28
+ const args = options.args || [];
29
+ const builder = new Builder();
30
+ if (browser === 'firefox') {
31
+ const firefoxOptions = new firefox.Options().addArguments(...args);
32
+ if (!headed)
33
+ firefoxOptions.addArguments('-headless');
34
+ builder.forBrowser(Browser.FIREFOX).setFirefoxOptions(firefoxOptions);
35
+ } else if (browser === 'edge') {
36
+ const edgeOptions = new edge.Options().addArguments(...args);
37
+ if (!headed)
38
+ edgeOptions.addArguments('--headless=new');
39
+ builder.forBrowser(Browser.EDGE).setEdgeOptions(edgeOptions);
40
+ } else if (browser === 'safari') {
41
+ builder.forBrowser(Browser.SAFARI);
42
+ } else {
43
+ const chromeOptions = new chrome.Options().addArguments(...args);
44
+ if (!headed)
45
+ chromeOptions.addArguments('--headless=new');
46
+ builder.forBrowser(Browser.CHROME).setChromeOptions(chromeOptions);
47
+ }
48
+ // Builder connects to SELENIUM_REMOTE_URL by itself when it is set.
49
+ const driver = await builder.build();
50
+ if (process.env.SELENIUM_REMOTE_URL)
51
+ driver.setFileDetector(new remote.FileDetector());
52
+ if (options.viewport)
53
+ await setViewportSize(driver, options.viewport.width, options.viewport.height);
54
+ return driver;
55
+ }
56
+
57
+ // Playwright viewports are inner sizes; WebDriver sizes the whole window.
58
+ async function setViewportSize(driver, width, height) {
59
+ await driver.manage().window().setRect({ width, height });
60
+ const [innerWidth, innerHeight] = await driver.executeScript('return [window.innerWidth, window.innerHeight];');
61
+ if (innerWidth !== width || innerHeight !== height)
62
+ await driver.manage().window().setRect({ width: 2 * width - innerWidth, height: 2 * height - innerHeight });
63
+ }
64
+
65
+ function toMatcher(value) {
66
+ return value instanceof RegExp ? { source: value.source, flags: value.flags } : String(value);
67
+ }
68
+
69
+ function format(value) {
70
+ if (value instanceof RegExp)
71
+ return String(value);
72
+ if (value instanceof Locator)
73
+ return value.toString();
74
+ if (typeof value === 'string')
75
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}'`;
76
+ if (value && typeof value === 'object')
77
+ return `{ ${Object.entries(value).map(([key, v]) => `${key}: ${format(v)}`).join(', ')} }`;
78
+ return String(value);
79
+ }
80
+
81
+ // Text value of a selector engine body: "text"i (substring), "text"s (exact) or /regex/flags.
82
+ function parseTextBody(body) {
83
+ const regex = /^\/(.*)\/([dgimsuvy]*)$/.exec(body);
84
+ if (regex)
85
+ return { v: { source: regex[1], flags: regex[2] }, exact: false };
86
+ if (body.endsWith('"s') || body.endsWith('"'))
87
+ return { v: JSON.parse(body.endsWith('"s') ? body.slice(0, -1) : body), exact: true };
88
+ if (body.endsWith('"i'))
89
+ return { v: JSON.parse(body.slice(0, -1)), exact: false };
90
+ return { v: body, exact: false };
91
+ }
92
+
93
+ function selectorStep(selector) {
94
+ if (selector.startsWith('xpath='))
95
+ return { t: 'xpath', v: selector.slice('xpath='.length) };
96
+ if (selector.startsWith('//') || selector.startsWith('..'))
97
+ return { t: 'xpath', v: selector };
98
+ if (selector.startsWith('css='))
99
+ return { t: 'css', v: selector.slice('css='.length) };
100
+ const hasText = /^internal:(has-text|has-not-text)=(.*)$/s.exec(selector);
101
+ if (hasText)
102
+ return { t: 'hasText', ...parseTextBody(hasText[2]), not: hasText[1] === 'has-not-text' };
103
+ const text = /^(?:internal:)?text=(.*)$/s.exec(selector);
104
+ if (text)
105
+ return { t: 'text', ...parseTextBody(text[1]) };
106
+ if (selector.startsWith('id='))
107
+ return { t: 'css', v: `[id=${JSON.stringify(selector.slice('id='.length))}]` };
108
+ if (/^[a-zA-Z_0-9-]+=/.test(selector) && !selector.startsWith('internal:attr='))
109
+ throw new Error(`testgen: selector engine not supported on Selenium: ${selector}`);
110
+ if (selector.startsWith('internal:'))
111
+ throw new Error(`testgen: selector not supported on Selenium: ${selector}`);
112
+ return { t: 'css', v: selector };
113
+ }
114
+
115
+ class Locator {
116
+ constructor(page, steps, description) {
117
+ this._page = page;
118
+ this._steps = steps;
119
+ this._description = description;
120
+ }
121
+
122
+ toString() {
123
+ return this._description;
124
+ }
125
+
126
+ _with(step, call) {
127
+ return new Locator(this._page, [...this._steps, step], `${this._description}.${call}`);
128
+ }
129
+
130
+ getByRole(role, options = {}) {
131
+ const step = { t: 'role', role };
132
+ if (options.name !== undefined)
133
+ step.name = toMatcher(options.name);
134
+ if (options.exact)
135
+ step.exact = true;
136
+ const attrs = {};
137
+ for (const key of ['checked', 'disabled', 'expanded', 'includeHidden', 'level', 'pressed', 'selected']) {
138
+ if (options[key] !== undefined)
139
+ attrs[key] = options[key];
140
+ }
141
+ if (Object.keys(attrs).length)
142
+ step.attrs = attrs;
143
+ return this._with(step, `getByRole(${format(role)}${Object.keys(options).length ? ', ' + format(options) : ''})`);
144
+ }
145
+
146
+ getByText(text, options = {}) {
147
+ return this._byText('text', 'getByText', text, options);
148
+ }
149
+
150
+ getByLabel(text, options = {}) {
151
+ return this._byText('label', 'getByLabel', text, options);
152
+ }
153
+
154
+ getByPlaceholder(text, options = {}) {
155
+ return this._byText('placeholder', 'getByPlaceholder', text, options);
156
+ }
157
+
158
+ getByAltText(text, options = {}) {
159
+ return this._byText('alt', 'getByAltText', text, options);
160
+ }
161
+
162
+ getByTitle(text, options = {}) {
163
+ return this._byText('title', 'getByTitle', text, options);
164
+ }
165
+
166
+ _byText(kind, method, text, options) {
167
+ const step = { t: kind, v: toMatcher(text), exact: !!options.exact };
168
+ return this._with(step, `${method}(${format(text)}${options.exact ? ', { exact: true }' : ''})`);
169
+ }
170
+
171
+ getByTestId(testId) {
172
+ return this._with({ t: 'testid', attr: this._page._testIdAttribute, v: toMatcher(testId) }, `getByTestId(${format(testId)})`);
173
+ }
174
+
175
+ locator(selectorOrLocator, options = {}) {
176
+ const result = selectorOrLocator instanceof Locator
177
+ ? new Locator(this._page, [...this._steps, ...selectorOrLocator._steps], `${this._description}.locator(${selectorOrLocator})`)
178
+ : this._with(selectorStep(selectorOrLocator), `locator(${format(selectorOrLocator)})`);
179
+ return Object.keys(options).length ? result.filter(options) : result;
180
+ }
181
+
182
+ frameLocator(selector) {
183
+ return this.locator(selector).contentFrame();
184
+ }
185
+
186
+ contentFrame() {
187
+ return this._with({ t: 'frame' }, 'contentFrame()');
188
+ }
189
+
190
+ filter(options = {}) {
191
+ let result = this;
192
+ if (options.hasText !== undefined)
193
+ result = result._with({ t: 'hasText', v: toMatcher(options.hasText) }, `filter({ hasText: ${format(options.hasText)} })`);
194
+ if (options.hasNotText !== undefined)
195
+ result = result._with({ t: 'hasText', v: toMatcher(options.hasNotText), not: true }, `filter({ hasNotText: ${format(options.hasNotText)} })`);
196
+ if (options.has)
197
+ result = result._with({ t: 'has', steps: options.has._steps }, `filter({ has: ${options.has} })`);
198
+ if (options.hasNot)
199
+ result = result._with({ t: 'has', steps: options.hasNot._steps, not: true }, `filter({ hasNot: ${options.hasNot} })`);
200
+ if (options.visible !== undefined)
201
+ result = result._with({ t: 'visible', v: !!options.visible }, `filter({ visible: ${!!options.visible} })`);
202
+ return result;
203
+ }
204
+
205
+ nth(index) {
206
+ return this._with({ t: 'nth', v: index }, `nth(${index})`);
207
+ }
208
+
209
+ first() {
210
+ return this._with({ t: 'nth', v: 0 }, 'first()');
211
+ }
212
+
213
+ last() {
214
+ return this._with({ t: 'nth', v: -1 }, 'last()');
215
+ }
216
+
217
+ and(locator) {
218
+ return this._with({ t: 'and', steps: locator._steps }, `and(${locator})`);
219
+ }
220
+
221
+ or(locator) {
222
+ return this._with({ t: 'or', steps: locator._steps }, `or(${locator})`);
223
+ }
224
+
225
+ // Elements currently matching the locator (no waiting).
226
+ async elements() {
227
+ const driver = this._page._driver;
228
+ await this._page._activate();
229
+ await driver.switchTo().defaultContent();
230
+ let segment = [];
231
+ for (const step of this._steps) {
232
+ if (step.t !== 'frame') {
233
+ segment.push(step);
234
+ continue;
235
+ }
236
+ const [frame] = await driver.executeScript(testgenResolve, JSON.stringify(segment));
237
+ if (!frame)
238
+ return [];
239
+ await driver.switchTo().frame(frame);
240
+ segment = [];
241
+ }
242
+ return driver.executeScript(testgenResolve, JSON.stringify(segment));
243
+ }
244
+
245
+ async count() {
246
+ return (await this.elements()).length;
247
+ }
248
+
249
+ // Waits for the first matching element to be ready, then runs `action` on it.
250
+ async _act(name, action, { visible = true, enabled = false } = {}) {
251
+ const deadline = Date.now() + this._page._timeout;
252
+ let reason = '';
253
+ for (;;) {
254
+ try {
255
+ const [element] = await this.elements();
256
+ if (!element)
257
+ reason = 'no element matches';
258
+ else if (visible && !await element.isDisplayed())
259
+ reason = 'element is not visible';
260
+ else if (enabled && !await element.isEnabled())
261
+ reason = 'element is not enabled';
262
+ else {
263
+ const result = await action(element);
264
+ await this._page._handleDialogs();
265
+ return result;
266
+ }
267
+ } catch (error) {
268
+ if (!isRetryable(error))
269
+ throw error;
270
+ reason = error.message;
271
+ }
272
+ if (Date.now() > deadline)
273
+ throw new Error(`${name}: timeout ${this._page._timeout}ms exceeded waiting for ${this} (${reason})`);
274
+ await sleep(100);
275
+ }
276
+ }
277
+
278
+ async click(options = {}) {
279
+ const driver = this._page._driver;
280
+ const simple = !options.button && !options.modifiers && !options.clickCount && !options.position;
281
+ return this._act('click', async element => {
282
+ if (simple)
283
+ return element.click();
284
+ const button = { right: Button.RIGHT, middle: Button.MIDDLE }[options.button] ?? Button.LEFT;
285
+ const actions = driver.actions({ async: true });
286
+ const keys = (options.modifiers || []).map(modifierKey);
287
+ keys.forEach(key => actions.keyDown(key));
288
+ await moveTo(actions, element, options.position);
289
+ for (let i = 0; i < (options.clickCount || 1); i++)
290
+ actions.press(button).release(button);
291
+ keys.reverse().forEach(key => actions.keyUp(key));
292
+ return actions.perform();
293
+ }, { enabled: true });
294
+ }
295
+
296
+ async dblclick() {
297
+ const driver = this._page._driver;
298
+ return this._act('dblclick', element => driver.actions({ async: true }).doubleClick(element).perform(), { enabled: true });
299
+ }
300
+
301
+ async hover(options = {}) {
302
+ const driver = this._page._driver;
303
+ return this._act('hover', async element => {
304
+ const actions = driver.actions({ async: true });
305
+ await moveTo(actions, element, options.position);
306
+ return actions.perform();
307
+ });
308
+ }
309
+
310
+ async fill(value) {
311
+ return this._act('fill', async element => {
312
+ await element.clear();
313
+ if (value)
314
+ await element.sendKeys(value);
315
+ }, { enabled: true });
316
+ }
317
+
318
+ async press(key) {
319
+ return this._act('press', element => element.sendKeys(toKeys(key)));
320
+ }
321
+
322
+ async check() {
323
+ return this._setChecked(true);
324
+ }
325
+
326
+ async uncheck() {
327
+ return this._setChecked(false);
328
+ }
329
+
330
+ async _setChecked(checked) {
331
+ return this._act(checked ? 'check' : 'uncheck', async element => {
332
+ if (await isChecked(element) !== checked)
333
+ await element.click();
334
+ }, { enabled: true });
335
+ }
336
+
337
+ async selectOption(values) {
338
+ const list = Array.isArray(values) ? values : [values];
339
+ return this._act('selectOption', async element => {
340
+ const select = new Select(element);
341
+ if (await select.isMultiple())
342
+ await select.deselectAll();
343
+ for (const value of list) {
344
+ try {
345
+ await select.selectByValue(value);
346
+ } catch {
347
+ await select.selectByVisibleText(value);
348
+ }
349
+ }
350
+ }, { enabled: true });
351
+ }
352
+
353
+ async setInputFiles(files) {
354
+ const list = (Array.isArray(files) ? files : [files]).map(file => path.resolve(file));
355
+ return this._act('setInputFiles', element => element.sendKeys(list.join('\n')), { visible: false });
356
+ }
357
+ }
358
+
359
+ async function moveTo(actions, element, position) {
360
+ if (!position)
361
+ return actions.move({ origin: element });
362
+ // Playwright positions are relative to the top-left corner, WebDriver offsets to the center.
363
+ const rect = await element.getRect();
364
+ return actions.move({ origin: element, x: Math.round(position.x - rect.width / 2), y: Math.round(position.y - rect.height / 2) });
365
+ }
366
+
367
+ class Page extends Locator {
368
+ static async attach(driver, options = {}) {
369
+ return new Page(driver, await driver.getWindowHandle(), options);
370
+ }
371
+
372
+ constructor(driver, handle, options = {}) {
373
+ super(null, [], 'page');
374
+ this._page = this;
375
+ this._driver = driver;
376
+ this._handle = handle;
377
+ this._options = options;
378
+ this._testIdAttribute = options.testIdAttribute || 'data-testid';
379
+ this._timeout = options.timeout || defaultTimeout;
380
+ this._dialogHandlers = [];
381
+ }
382
+
383
+ async _activate() {
384
+ const current = await this._driver.getWindowHandle().catch(() => undefined);
385
+ if (current !== this._handle)
386
+ await this._driver.switchTo().window(this._handle);
387
+ }
388
+
389
+ async goto(url) {
390
+ await this._activate();
391
+ await this._driver.get(url);
392
+ }
393
+
394
+ async close() {
395
+ await this._activate();
396
+ await this._driver.close();
397
+ }
398
+
399
+ context() {
400
+ return {
401
+ newPage: async () => {
402
+ await this._driver.switchTo().newWindow('tab');
403
+ return new Page(this._driver, await this._driver.getWindowHandle(), this._options);
404
+ },
405
+ };
406
+ }
407
+
408
+ // Supports 'popup': resolves to the window opened after this call.
409
+ waitForEvent(event) {
410
+ if (event !== 'popup')
411
+ return Promise.reject(new Error(`testgen: page.waitForEvent('${event}') is not supported on Selenium`));
412
+ const known = this._driver.getAllWindowHandles();
413
+ const popup = (async () => {
414
+ const before = new Set(await known);
415
+ const deadline = Date.now() + this._timeout;
416
+ for (;;) {
417
+ const handle = (await this._driver.getAllWindowHandles()).find(h => !before.has(h));
418
+ if (handle)
419
+ return new Page(this._driver, handle, this._options);
420
+ if (Date.now() > deadline)
421
+ throw new Error(`waitForEvent('popup'): timeout ${this._timeout}ms exceeded`);
422
+ await sleep(100);
423
+ }
424
+ })();
425
+ // Avoid an unhandled rejection when the test fails before awaiting the popup.
426
+ popup.catch(() => {});
427
+ return popup;
428
+ }
429
+
430
+ // Supports 'dialog': the handler runs for the next alert/confirm/prompt opened by an action.
431
+ once(event, handler) {
432
+ if (event !== 'dialog')
433
+ throw new Error(`testgen: page.once('${event}') is not supported on Selenium`);
434
+ this._dialogHandlers.push(handler);
435
+ }
436
+
437
+ async _handleDialogs() {
438
+ if (!this._dialogHandlers.length)
439
+ return;
440
+ let alert;
441
+ try {
442
+ alert = await this._driver.wait(until.alertIsPresent(), 2000);
443
+ } catch {
444
+ return;
445
+ }
446
+ const dialog = new Dialog(alert, await alert.getText());
447
+ await this._dialogHandlers.shift()(dialog);
448
+ await dialog._settled;
449
+ }
450
+ }
451
+
452
+ class Dialog {
453
+ constructor(alert, message) {
454
+ this._alert = alert;
455
+ this._message = message;
456
+ this._settled = Promise.resolve();
457
+ }
458
+
459
+ message() {
460
+ return this._message;
461
+ }
462
+
463
+ dismiss() {
464
+ return this._settled = this._alert.dismiss();
465
+ }
466
+
467
+ accept(promptText) {
468
+ this._settled = (async () => {
469
+ if (promptText !== undefined)
470
+ await this._alert.sendKeys(promptText);
471
+ await this._alert.accept();
472
+ })();
473
+ return this._settled;
474
+ }
475
+ }
476
+
477
+ function expect(locator) {
478
+ return new LocatorAssertions(locator, false);
479
+ }
480
+
481
+ class LocatorAssertions {
482
+ constructor(locator, negated) {
483
+ this._locator = locator;
484
+ this._negated = negated;
485
+ }
486
+
487
+ get not() {
488
+ return new LocatorAssertions(this._locator, !this._negated);
489
+ }
490
+
491
+ // Polls until `check` passes (or fails, when negated) for the first matching element.
492
+ async _poll(name, expected, check) {
493
+ const page = this._locator._page;
494
+ const deadline = Date.now() + page._timeout;
495
+ let actual = '<element not found>';
496
+ for (;;) {
497
+ try {
498
+ const [element] = await this._locator.elements();
499
+ const result = element ? await check(element) : { pass: false, actual: '<element not found>' };
500
+ if (result.pass !== this._negated)
501
+ return;
502
+ actual = result.actual;
503
+ } catch (error) {
504
+ if (!isRetryable(error))
505
+ throw error;
506
+ actual = error.message;
507
+ }
508
+ if (Date.now() > deadline) {
509
+ throw new assert.AssertionError({
510
+ message: `expect(${this._locator})${this._negated ? '.not' : ''}.${name}(${expected === undefined ? '' : format(expected)}) failed after ${page._timeout}ms\nReceived: ${actual}`,
511
+ });
512
+ }
513
+ await sleep(100);
514
+ }
515
+ }
516
+
517
+ async toBeVisible() {
518
+ return this._poll('toBeVisible', undefined, async element => {
519
+ const visible = await element.isDisplayed();
520
+ return { pass: visible, actual: visible ? 'visible' : 'hidden' };
521
+ });
522
+ }
523
+
524
+ async toHaveText(expected) {
525
+ return this._poll('toHaveText', expected, async element => {
526
+ const text = normalize(await element.getProperty('textContent'));
527
+ return { pass: textMatches(text, expected, true), actual: text };
528
+ });
529
+ }
530
+
531
+ async toContainText(expected) {
532
+ return this._poll('toContainText', expected, async element => {
533
+ const text = normalize(await element.getProperty('textContent'));
534
+ return { pass: textMatches(text, expected, false), actual: text };
535
+ });
536
+ }
537
+
538
+ async toHaveValue(expected) {
539
+ return this._poll('toHaveValue', expected, async element => {
540
+ const value = String(await element.getProperty('value'));
541
+ return { pass: expected instanceof RegExp ? expected.test(value) : value === expected, actual: value };
542
+ });
543
+ }
544
+
545
+ async toBeEmpty() {
546
+ return this._poll('toBeEmpty', undefined, async element => {
547
+ const tag = await element.getTagName();
548
+ const value = /^(input|textarea)$/i.test(tag) ? await element.getProperty('value') : normalize(await element.getProperty('textContent'));
549
+ return { pass: !value, actual: value };
550
+ });
551
+ }
552
+
553
+ async toBeChecked() {
554
+ return this._poll('toBeChecked', undefined, async element => {
555
+ const checked = await isChecked(element);
556
+ return { pass: checked, actual: checked ? 'checked' : 'unchecked' };
557
+ });
558
+ }
559
+ }
560
+
561
+ function normalize(text) {
562
+ return String(text ?? '').replace(/​/g, '').trim().replace(/\s+/g, ' ');
563
+ }
564
+
565
+ function textMatches(text, expected, whole) {
566
+ if (expected instanceof RegExp)
567
+ return expected.test(text);
568
+ return whole ? text === normalize(expected) : text.includes(normalize(expected));
569
+ }
570
+
571
+ async function isChecked(element) {
572
+ const tag = await element.getTagName();
573
+ const type = String(await element.getAttribute('type')).toLowerCase();
574
+ if (tag.toLowerCase() === 'input' && (type === 'checkbox' || type === 'radio'))
575
+ return element.isSelected();
576
+ return await element.getAttribute('aria-checked') === 'true';
577
+ }
578
+
579
+ function isRetryable(error) {
580
+ const retryable = ['StaleElementReferenceError', 'ElementNotInteractableError', 'ElementClickInterceptedError',
581
+ 'NoSuchElementError', 'NoSuchFrameError', 'InvalidElementStateError', 'NoSuchWindowError'];
582
+ if (retryable.includes(error.name))
583
+ return true;
584
+ // Scripts fail while the page navigates; real script errors carry a testgen or syntax message.
585
+ return error.name === 'JavascriptError' && !/testgen:|SyntaxError|not a valid/.test(error.message);
586
+ }
587
+
588
+ const namedKeys = {
589
+ Enter: Key.ENTER, Tab: Key.TAB, Escape: Key.ESCAPE, Backspace: Key.BACK_SPACE, Delete: Key.DELETE,
590
+ ArrowUp: Key.ARROW_UP, ArrowDown: Key.ARROW_DOWN, ArrowLeft: Key.ARROW_LEFT, ArrowRight: Key.ARROW_RIGHT,
591
+ Home: Key.HOME, End: Key.END, PageUp: Key.PAGE_UP, PageDown: Key.PAGE_DOWN, Insert: Key.INSERT, Space: ' ',
592
+ F1: Key.F1, F2: Key.F2, F3: Key.F3, F4: Key.F4, F5: Key.F5, F6: Key.F6,
593
+ F7: Key.F7, F8: Key.F8, F9: Key.F9, F10: Key.F10, F11: Key.F11, F12: Key.F12,
594
+ };
595
+
596
+ function modifierKey(name) {
597
+ switch (name) {
598
+ case 'Alt': return Key.ALT;
599
+ case 'Control': return Key.CONTROL;
600
+ case 'Meta': return Key.META;
601
+ case 'Shift': return Key.SHIFT;
602
+ case 'ControlOrMeta': return process.platform === 'darwin' ? Key.META : Key.CONTROL;
603
+ }
604
+ throw new Error(`testgen: unknown modifier ${name}`);
605
+ }
606
+
607
+ // Playwright key names ("Enter", "Shift+Tab", "ControlOrMeta+a") → WebDriver keys.
608
+ function toKeys(shortcut) {
609
+ const parts = shortcut.split(/\+(?=.)/);
610
+ const key = parts.pop();
611
+ const modifiers = parts.map(modifierKey);
612
+ const keyValue = namedKeys[key] ?? key;
613
+ return modifiers.length ? Key.chord(...modifiers, keyValue) : keyValue;
614
+ }
615
+
616
+ /* @testgen-engine */
617
+
618
+ module.exports = { createDriver, setViewportSize, Page, Locator, Dialog, expect };