@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.
package/NOTICE ADDED
@@ -0,0 +1,29 @@
1
+ testgen (@xylentis/testgen)
2
+ Copyright (C) 2026 Xylentis (https://portal.xylentis.com)
3
+
4
+ This program is free software: you can redistribute it and/or modify it under the terms of the
5
+ GNU Affero General Public License as published by the Free Software Foundation, version 3 only
6
+ (SPDX: AGPL-3.0-only). This program is distributed in the hope that it will be useful, but
7
+ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
8
+ PARTICULAR PURPOSE. See the GNU Affero General Public License in the LICENSE file for details.
9
+
10
+ Additional permission under section 7 of the GNU Affero General Public License, version 3:
11
+
12
+ Test files generated by testgen, and the support files testgen writes next to them
13
+ (runtime/selenium/engine.js, runtime/selenium-java/TestgenSupport.java,
14
+ runtime/selenium-js/testgen-support.js and copies of them), may be used, copied, modified
15
+ and distributed under terms of your choice, as part of test code produced with testgen.
16
+ This permission does not cover testgen itself, nor the use of these files in another test
17
+ generator or code generator.
18
+
19
+ Third-party code:
20
+
21
+ Files under src/vendor/playwright (compiled to dist/vendor/playwright) come from Playwright
22
+ (https://github.com/microsoft/playwright), Copyright (c) Microsoft Corporation, licensed under
23
+ the Apache License, Version 2.0 (see the LICENSE file next to them). Playwright's NOTICE reads:
24
+
25
+ Playwright
26
+ Copyright (c) Microsoft Corporation
27
+
28
+ This software contains code derived from the Puppeteer project (https://github.com/puppeteer/puppeteer),
29
+ available under the Apache 2.0 license (https://github.com/puppeteer/puppeteer/blob/master/LICENSE).
package/README.md CHANGED
@@ -1,3 +1,196 @@
1
- # Temporary Holding Version
1
+ # Automation Test Code: testgen
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Ghi thao tác trên web **một lần** bằng recorder của Playwright, rồi sinh test UI cho **nhiều framework và nhiều ngôn ngữ**.
4
+ Cách dùng giống `npx playwright codegen`, nhưng không chỉ sinh code Playwright mà còn sinh cả Selenium, và thêm ngôn ngữ Go.
5
+
6
+ ![Giao diện testgen](https://cdn.jsdelivr.net/npm/@xylentis/testgen@1/docs/testgen-ui.png)
7
+
8
+ | Target | Framework | Ngôn ngữ | File sinh ra |
9
+ | --- | --- | --- | --- |
10
+ | `playwright-ts` | Playwright Test (`@playwright/test`) | TypeScript | `login.spec.ts` |
11
+ | `playwright-go` | [playwright-go](https://github.com/mxschmitt/playwright-go) + testify | Go | `login_test.go` |
12
+ | `selenium-java` | Selenium 4 + JUnit 5 | Java 17+ | `LoginTest.java` + `TestgenSupport.java` |
13
+ | `selenium-js` | Selenium 4 + Mocha | JavaScript | `login.spec.js` + `testgen-support.js` |
14
+
15
+ ## Cài đặt
16
+
17
+ Cần **Node.js 22+** (khuyên dùng bản LTS 24).
18
+
19
+ ```bash
20
+ npm install -g @xylentis/testgen # cài lệnh `testgen`
21
+ testgen ui
22
+
23
+ npx @xylentis/testgen ui # hoặc chạy luôn, không cần cài
24
+ npm i -D @xylentis/testgen # hoặc cài vào project, rồi gọi `npx testgen ...`
25
+ ```
26
+
27
+ Để ghi thao tác cần một trình duyệt. Nếu máy đã có **Google Chrome** thì chọn Chrome là xong. Nếu muốn dùng Chromium của Playwright thì tải về bằng:
28
+
29
+ ```bash
30
+ npx playwright-core install chromium
31
+ ```
32
+
33
+ ## Dùng giao diện (khuyên dùng)
34
+
35
+ ```bash
36
+ testgen ui # mở http://127.0.0.1:9323
37
+ ```
38
+
39
+ 1. Nhập URL, chọn trình duyệt rồi bấm **Record**.
40
+ 2. Một cửa sổ trình duyệt mở ra. Bạn thao tác như người dùng thật: click, gõ, chọn…
41
+ - Thanh công cụ của recorder nằm ở đầu trang, dùng để thêm assert: *Assert visibility*, *Assert text*, *Assert value*.
42
+ - Cửa sổ Playwright Inspector được ẩn đi, chỉ còn trình duyệt bạn đang thao tác. Muốn hiện lại thì dùng `testgen record --inspector`.
43
+ 3. Trong khi bạn thao tác, giao diện testgen cập nhật **danh sách bước** và **code của cả 4 target**.
44
+ 4. Đóng trình duyệt hoặc bấm **Stop** khi xong.
45
+ 5. Với từng target, bạn có thể **Copy**, **Download** hoặc **Save files** để lưu thẳng vào thư mục project. Đường dẫn tính từ thư mục bạn chạy `testgen ui`.
46
+
47
+ Một số thao tác khác:
48
+
49
+ - **Đổi Test name / Package / Test id attribute:** code sinh lại ngay.
50
+ - **Download recording:** lưu file `.jsonl` để lần sau sinh lại.
51
+ - **Open recording…:** mở lại một file đã lưu.
52
+
53
+ ## Dùng dòng lệnh
54
+
55
+ ```bash
56
+ # Ghi rồi sinh luôn (đóng trình duyệt là sinh). Mặc định ra ./generated/<target>/
57
+ testgen record https://your-app.example --channel chrome
58
+
59
+ # Chỉ một vài target, chọn thư mục ra
60
+ testgen record https://your-app.example -t selenium-java,playwright-go -d tests/generated
61
+
62
+ # Sinh lại từ một bản ghi đã lưu
63
+ testgen generate generated/recording.jsonl -t selenium-java -o src/test/java/com/acme/e2e/LoginTest.java
64
+ testgen generate generated/recording.jsonl -t playwright-go -o e2e/login_test.go
65
+ testgen generate generated/recording.jsonl -d out # tất cả target, mỗi target một thư mục
66
+
67
+ testgen targets # liệt kê target
68
+ ```
69
+
70
+ | Tùy chọn | Ý nghĩa |
71
+ | --- | --- |
72
+ | `-t, --target` | Danh sách target, cách nhau bằng dấu phẩy. Mặc định là tất cả. |
73
+ | `-o, --output` | File test cần ghi, chỉ dùng khi có 1 target. File hỗ trợ (support) được đặt cạnh file test. |
74
+ | `-d, --out-dir` | Thư mục ra. Nếu có nhiều target thì mỗi target một thư mục con. |
75
+ | `-n, --name` | Tên test. Mặc định lấy từ tên file `--output`, nếu không có thì là `recorded`. |
76
+ | `--package` | Package cho Java/Go. Java tự lấy từ đường dẫn sau `src/test/java/`, Go tự lấy từ tên thư mục. |
77
+ | `--test-id-attribute` | Thuộc tính dùng cho `getByTestId` (mặc định `data-testid`). |
78
+ | `--save` | (`record`) Nơi lưu bản ghi `.jsonl`. |
79
+ | `--browser`, `--channel`, `--device`, `--viewport-size`, `--lang`, `--timezone`, `--color-scheme`, `--user-agent`, `--ignore-https-errors`, `--load-storage`, `--save-storage` | (`record`) Giống hệt `playwright codegen`. |
80
+ | `--inspector` | (`record`) Hiện thêm cửa sổ Playwright Inspector (mặc định ẩn). |
81
+
82
+ ## Chạy test đã sinh
83
+
84
+ Trong `e2e/projects/` có sẵn project mẫu cho từng target. Đó là đúng các project dùng để kiểm thử testgen, nên copy về dùng ngay được.
85
+
86
+ **playwright-ts**: cần `@playwright/test` (xem `e2e/projects/playwright-ts`):
87
+
88
+ ```bash
89
+ npm i -D @playwright/test && npx playwright install chromium
90
+ npx playwright test
91
+ ```
92
+
93
+ **playwright-go**: lưu ý module path hiện tại là `github.com/mxschmitt/playwright-go`:
94
+
95
+ ```bash
96
+ go get github.com/mxschmitt/playwright-go@v0.6201.1 github.com/stretchr/testify
97
+ go run github.com/mxschmitt/playwright-go/cmd/playwright install --with-deps chromium
98
+ go test ./... # HEADED=1 go test ./... để xem trình duyệt
99
+ ```
100
+
101
+ **selenium-java**: cần `selenium-java` và `junit-jupiter` (xem `e2e/projects/selenium-java/pom.xml`). `LoginTest.java` và `TestgenSupport.java` phải nằm cùng package. Chạy bằng `mvn test`.
102
+
103
+ **selenium-js**: cần `selenium-webdriver` và `mocha` (xem `e2e/projects/selenium-js`). `testgen-support.js` phải nằm cạnh file test. Chạy bằng `npx mocha`.
104
+
105
+ Biến môi trường mà test Selenium hiểu:
106
+
107
+ - `HEADED=1`: hiện trình duyệt.
108
+ - `SELENIUM_REMOTE_URL=http://grid:4444`: chạy trên Selenium Grid.
109
+ - `TESTGEN_TIMEOUT=15000`: thời gian tự chờ, đơn vị ms, mặc định 10000.
110
+
111
+ ### Code Selenium trông như thế nào
112
+
113
+ Selenium không có `getByRole` hay `getByText`, cũng không tự chờ element. Vì vậy test Selenium đi kèm một **file hỗ trợ** (`TestgenSupport.java` / `testgen-support.js`) để code sinh ra vẫn gọn và chạy ổn định:
114
+
115
+ ```java
116
+ page.navigate("http://127.0.0.1:4321/");
117
+ page.getByRole("textbox", "Email").fill("qa@example.com");
118
+ page.getByLabel("Country").selectOption("jp");
119
+ page.getByRole("button", "Sign in").click();
120
+ page.getByTestId("welcome").shouldContainText("Welcome, qa@example.com (jp)");
121
+ page.getByRole("listitem").filterHasText("Buy milk Delete").getByRole("button").click();
122
+ var page1 = page.waitForPopup(() -> page.getByRole("link", "Open help").click());
123
+ ```
124
+
125
+ Bản JavaScript dùng **đúng API của Playwright** (`page.getByRole('button', { name: 'Sign in' })`, `expect(...).toHaveText(...)`). Thân test vì thế giống hệt bản `playwright-ts`, chỉ khác phần khởi tạo driver.
126
+
127
+ Bên dưới vẫn là Selenium WebDriver: các hành động dùng `WebElement.click()`, `sendKeys()`, `Select`, `Actions`. Biến `driver` vẫn có sẵn trong test nếu bạn cần viết thêm code Selenium thuần.
128
+
129
+ ## Cách hoạt động
130
+
131
+ ```
132
+ Bạn thao tác trên trình duyệt
133
+ │ recorder của Playwright (như `playwright codegen --target=jsonl`, Inspector ẩn), ghi lại mỗi bước
134
+ ▼
135
+ recording.jsonl ── danh sách bước, chưa gắn với ngôn ngữ nào (click, fill, assertText…)
136
+ │ selector → chuỗi locator: dùng lại mã của Playwright (src/vendor/playwright)
137
+ ▼
138
+ Generators (src/generators/*)
139
+ ├─ playwright-ts giống `playwright codegen --target playwright-test`
140
+ ├─ playwright-go API playwright-go + require.NoError
141
+ ├─ selenium-java JUnit 5 + TestgenSupport.java
142
+ └─ selenium-js Mocha + testgen-support.js
143
+ └─ runtime/selenium/engine.js: chạy trong trang (executeScript)
144
+ để tìm element theo role / text / label / test id… như Playwright
145
+ ```
146
+
147
+ - **Recorder:** dùng nguyên recorder của Playwright, nên cách chọn locator (ưu tiên test id, role + tên, label…) giống hệt Playwright codegen.
148
+ - **Engine locator cho Selenium:** được kiểm tra bằng cách so kết quả với chính Playwright trên 70 locator mẫu (`test/engine.test.ts`).
149
+
150
+ ## Giới hạn hiện tại
151
+
152
+ - **Định dạng JSONL là định dạng nội bộ của Playwright.** Vì vậy `playwright-core` được ghim ở `1.63.0`. Khi nâng cấp phải chạy lại `npm test` và `npm run e2e` (xem `src/vendor/playwright/README.md`).
153
+ - **Selenium không làm được các việc sau**, nên chỗ đó sẽ có comment `// TODO(testgen)`:
154
+ - chờ download;
155
+ - assert aria snapshot (thay bằng kiểm tra visible);
156
+ - click chuột giữa;
157
+ - giả lập thiết bị đầy đủ (chỉ áp dụng viewport và user agent);
158
+ - `colorScheme` / `timezone` / `geolocation`;
159
+ - lưu storage state.
160
+ - **`getByRole` trên Selenium chỉ gần đúng**, vì thuật toán tính accessible name được đơn giản hóa. Khi gặp sai lệch, hãy thêm `data-testid` cho app, hoặc thêm trường hợp đó vào `test/engine.test.ts`.
161
+ - **Bản ghi không lưu đường dẫn đầy đủ của file upload**, chỉ có tên file. Cần sửa đường dẫn trong `setInputFiles(...)`.
162
+
163
+ ## Phát triển
164
+
165
+ Package trên npm chỉ chứa bản build. Để sửa code thì làm việc trên repo:
166
+
167
+ ```bash
168
+ npm ci # tự build (script prepare); gọi tool bằng `node dist/cli.js`
169
+ npm test # vitest: so engine với Playwright, snapshot code sinh ra, test giao diện (cần Chrome)
170
+ npm run e2e # sinh test cho cả 4 target rồi chạy trên app demo e2e/app (cần Go, JDK 17+ và Maven, Chrome)
171
+ E2E_TARGETS="selenium-js playwright-go" npm run e2e
172
+ ```
173
+
174
+ - **Ghi lại bản demo:** chạy app (`e2e/app`) ở cổng 4321, rồi chạy `node e2e/record-demo.mjs`. Script này điều khiển recorder thật của Playwright, không cần người thao tác.
175
+ - **Code sinh ra cho 2 bản ghi mẫu** được lưu thành snapshot trong `test/__snapshots__/`. Đổi generator thì chạy `npx vitest run -u` và review diff.
176
+ - **Thêm target mới:**
177
+ 1. Viết một `Generator` trong `src/generators/` và đăng ký trong `src/generators/index.ts`.
178
+ 2. Thêm project mẫu trong `e2e/projects/`.
179
+ 3. Thêm bước chạy trong `e2e/run.sh`.
180
+
181
+ ```
182
+ src/ CLI (cli.ts), đọc bản ghi, generators, server giao diện (ui/server.ts)
183
+ src/vendor/ mã lấy từ Playwright (Apache-2.0), xem NOTICE
184
+ runtime/ file hỗ trợ Selenium (Java, JS) + engine locator chạy trong trang
185
+ ui/ giao diện web (HTML/CSS/JS thuần, không cần build)
186
+ test/ unit test, fixture, snapshot
187
+ e2e/ app demo, bản ghi thật, project mẫu cho từng target, run.sh
188
+ ```
189
+
190
+ ## License
191
+
192
+ [GNU AGPL-3.0-only](LICENSE), Copyright (C) 2026 Xylentis.
193
+
194
+ - **testgen:** ai sửa testgen rồi phát hành lại, kể cả đem chạy thành dịch vụ qua mạng, đều phải công bố mã nguồn bản đã sửa theo AGPL.
195
+ - **Code sinh ra:** test do testgen sinh ra và các file hỗ trợ đi kèm (`TestgenSupport.java`, `testgen-support.js`, engine) được dùng, sửa và phân phối **tự do** trong test của bạn, theo điều khoản bổ sung ở mục 7 của AGPL. Điều khoản này không cho phép dùng các file đó để làm một công cụ sinh code khác. Xem [NOTICE](NOTICE).
196
+ - **Code lấy từ Playwright:** các file trong `src/vendor/playwright` vẫn theo Apache License 2.0.
package/dist/cli.js ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { parseArgs } from 'node:util';
6
+ import { generateTargets, writeTargets } from './generate.js';
7
+ import { generators } from './generators/index.js';
8
+ import { startCodegen, waitForExit } from './record.js';
9
+ import { parseRecording } from './recording.js';
10
+ import { startUiServer } from './ui/server.js';
11
+ const usage = `testgen: record a UI test once with the Playwright recorder, then generate it for several
12
+ frameworks and languages.
13
+
14
+ Usage:
15
+ testgen ui [--port <port>] [--no-open] Web UI: record and watch the code of every target live
16
+ testgen record [url] [options] Record in a browser, generate when it is closed
17
+ testgen generate <recording.jsonl> [options] Generate tests from a saved recording
18
+ testgen targets List the available targets
19
+
20
+ Output options:
21
+ -t, --target <ids> Comma-separated targets (default: all of them)
22
+ -o, --output <file> Test file to write (one target only); support files go next to it
23
+ -d, --out-dir <dir> Output directory; one sub-directory per target when there are several
24
+ (default for record: ./generated)
25
+ -n, --name <name> Test name (default: taken from --output, else "recorded")
26
+ --package <name> Java or Go package (default: derived from the output path)
27
+ --test-id-attribute <a> Attribute used by getByTestId (default: data-testid)
28
+ --save <file.jsonl> record: where to keep the recording (default: <out-dir>/recording.jsonl)
29
+
30
+ Recorder options (record), same as playwright codegen:
31
+ --browser <chromium|firefox|webkit> --channel <chrome|msedge|...> --device <name>
32
+ --viewport-size <w,h> --lang <locale> --timezone <id> --color-scheme <light|dark>
33
+ --user-agent <ua> --ignore-https-errors --load-storage <file> --save-storage <file>
34
+ --inspector also show Playwright's Inspector window (hidden by default)
35
+ `;
36
+ const options = {
37
+ 'target': { type: 'string', short: 't' },
38
+ 'output': { type: 'string', short: 'o' },
39
+ 'out-dir': { type: 'string', short: 'd' },
40
+ 'name': { type: 'string', short: 'n' },
41
+ 'package': { type: 'string' },
42
+ 'test-id-attribute': { type: 'string', default: 'data-testid' },
43
+ 'save': { type: 'string' },
44
+ 'browser': { type: 'string', short: 'b' },
45
+ 'channel': { type: 'string' },
46
+ 'device': { type: 'string' },
47
+ 'viewport-size': { type: 'string' },
48
+ 'lang': { type: 'string' },
49
+ 'timezone': { type: 'string' },
50
+ 'color-scheme': { type: 'string' },
51
+ 'user-agent': { type: 'string' },
52
+ 'ignore-https-errors': { type: 'boolean' },
53
+ 'load-storage': { type: 'string' },
54
+ 'save-storage': { type: 'string' },
55
+ 'inspector': { type: 'boolean' },
56
+ 'port': { type: 'string', default: '9323' },
57
+ 'host': { type: 'string', default: '127.0.0.1' },
58
+ 'open': { type: 'boolean', default: true },
59
+ 'help': { type: 'boolean', short: 'h' },
60
+ };
61
+ async function main(argv) {
62
+ const { values, positionals } = parseArgs({ args: argv, options, allowPositionals: true, allowNegative: true });
63
+ const [command, ...rest] = positionals;
64
+ if (values.help || !command) {
65
+ process.stdout.write(usage);
66
+ return;
67
+ }
68
+ const targets = values.target ? values.target.split(',').map(t => t.trim()).filter(Boolean) : generators.map(g => g.id);
69
+ const output = {
70
+ targets,
71
+ output: values.output,
72
+ name: values.name,
73
+ packageName: values.package,
74
+ testIdAttribute: values['test-id-attribute'],
75
+ };
76
+ switch (command) {
77
+ case 'targets':
78
+ for (const generator of generators)
79
+ console.log(`${generator.id.padEnd(16)}${generator.framework} (${generator.language})`);
80
+ return;
81
+ case 'generate': {
82
+ const file = rest[0];
83
+ if (!file)
84
+ throw new Error('Usage: testgen generate <recording.jsonl> [options]');
85
+ const recording = parseRecording(fs.readFileSync(file, 'utf8'), file);
86
+ if (!values.output && !values['out-dir']) {
87
+ if (targets.length !== 1)
88
+ throw new Error('Pass --out-dir (or a single --target to print the test to stdout).');
89
+ const [{ files }] = generateTargets(recording, output);
90
+ process.stdout.write(files[0].content);
91
+ for (const support of files.slice(1))
92
+ console.error(`note: the test also needs ${support.path}; use --out-dir or --output to write it.`);
93
+ return;
94
+ }
95
+ report(writeTargets(generateTargets(recording, { ...output, outDir: values['out-dir'] })));
96
+ return;
97
+ }
98
+ case 'record': {
99
+ const outDir = values['out-dir'] ?? (values.output ? undefined : 'generated');
100
+ const save = path.resolve(values.save ?? path.join(outDir ?? path.dirname(values.output), 'recording.jsonl'));
101
+ fs.mkdirSync(path.dirname(save), { recursive: true });
102
+ fs.rmSync(save, { force: true });
103
+ const codegen = {
104
+ browser: values.browser,
105
+ channel: values.channel,
106
+ device: values.device,
107
+ viewportSize: values['viewport-size'],
108
+ lang: values.lang,
109
+ timezone: values.timezone,
110
+ colorScheme: values['color-scheme'],
111
+ userAgent: values['user-agent'],
112
+ ignoreHttpsErrors: values['ignore-https-errors'],
113
+ loadStorage: values['load-storage'],
114
+ saveStorage: values['save-storage'],
115
+ testIdAttribute: values['test-id-attribute'],
116
+ inspector: values.inspector,
117
+ };
118
+ console.log('Recording... interact with the page, then close the browser window (or press Ctrl+C) to generate the tests.');
119
+ const recorder = startCodegen(rest[0], save, codegen);
120
+ // Ctrl+C ends the recording, not testgen: the tests are still generated afterwards.
121
+ const stopRecorder = () => recorder.kill('SIGINT');
122
+ process.on('SIGINT', stopRecorder);
123
+ const code = await waitForExit(recorder);
124
+ process.off('SIGINT', stopRecorder);
125
+ if (!fs.existsSync(save))
126
+ throw new Error(`Nothing was recorded (the recorder exited with code ${code}).`);
127
+ console.log(`Recording saved to ${path.relative(process.cwd(), save)}`);
128
+ report(writeTargets(generateTargets(parseRecording(fs.readFileSync(save, 'utf8'), save), { ...output, outDir })));
129
+ return;
130
+ }
131
+ case 'ui': {
132
+ const server = await startUiServer({ port: Number(values.port), host: values.host, cwd: process.cwd() });
133
+ console.log(`testgen UI: ${server.url} (Ctrl+C to stop)`);
134
+ if (values.open)
135
+ openInBrowser(server.url);
136
+ const stop = () => server.close().then(() => process.exit(0));
137
+ process.on('SIGINT', stop);
138
+ process.on('SIGTERM', stop);
139
+ return;
140
+ }
141
+ default:
142
+ throw new Error(`Unknown command "${command}".\n\n${usage}`);
143
+ }
144
+ }
145
+ function report(files) {
146
+ for (const file of files)
147
+ console.log(` wrote ${path.relative(process.cwd(), file)}`);
148
+ }
149
+ function openInBrowser(url) {
150
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
151
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
152
+ spawn(command, args, { stdio: 'ignore', detached: true }).on('error', () => { }).unref();
153
+ }
154
+ main(process.argv.slice(2)).catch(error => {
155
+ console.error(`testgen: ${error.message}`);
156
+ process.exit(1);
157
+ });
158
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { findGenerator } from './generators/index.js';
4
+ import { nameFromFileName } from './naming.js';
5
+ export function generateTargets(recording, options) {
6
+ if (!options.targets.length)
7
+ throw new Error('Pick at least one target with --target.');
8
+ if (options.output && options.targets.length > 1)
9
+ throw new Error('--output takes a single target; use --out-dir for several targets.');
10
+ return options.targets.map(id => {
11
+ const generator = findGenerator(id);
12
+ const directory = options.output
13
+ ? path.dirname(options.output)
14
+ : path.join(options.outDir ?? '.', options.targets.length > 1 ? generator.id : '');
15
+ const name = options.name ?? (options.output ? nameFromFileName(options.output) : 'recorded');
16
+ const packageName = options.packageName ?? defaultPackage(generator, directory);
17
+ const files = generator.generate(recording, { name, packageName, testIdAttribute: options.testIdAttribute });
18
+ if (options.output)
19
+ files[0] = { ...files[0], path: path.basename(options.output) };
20
+ return { generator, directory, files };
21
+ });
22
+ }
23
+ // Java: package from the path below src/test/java (or src/main/java). Go: the directory name when it
24
+ // is a valid package name (the generator falls back to "e2e").
25
+ function defaultPackage(generator, directory) {
26
+ const absolute = path.resolve(directory).split(path.sep).join('/');
27
+ if (generator.language === 'Java') {
28
+ const match = /\/src\/(?:test|main|it)\/java\/(.+)$/.exec(absolute);
29
+ return match ? match[1].split('/').join('.') : undefined;
30
+ }
31
+ if (generator.language === 'Go' && /^[a-z][a-z0-9_]*$/.test(path.basename(absolute)))
32
+ return path.basename(absolute);
33
+ return undefined;
34
+ }
35
+ export function writeTargets(outputs) {
36
+ const written = [];
37
+ for (const { directory, files } of outputs) {
38
+ fs.mkdirSync(directory, { recursive: true });
39
+ for (const file of files) {
40
+ const filePath = path.join(directory, file.path);
41
+ fs.writeFileSync(filePath, file.content);
42
+ written.push(filePath);
43
+ }
44
+ }
45
+ return written;
46
+ }
47
+ //# sourceMappingURL=generate.js.map
@@ -0,0 +1,12 @@
1
+ import { playwrightGo } from './playwright-go.js';
2
+ import { playwrightTs } from './playwright-ts.js';
3
+ import { seleniumJava } from './selenium-java.js';
4
+ import { seleniumJs } from './selenium-js.js';
5
+ export const generators = [playwrightTs, playwrightGo, seleniumJava, seleniumJs];
6
+ export function findGenerator(id) {
7
+ const generator = generators.find(g => g.id === id);
8
+ if (!generator)
9
+ throw new Error(`Unknown target "${id}". Available targets: ${generators.map(g => g.id).join(', ')}`);
10
+ return generator;
11
+ }
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,102 @@
1
+ import { asLocator } from '../vendor/playwright/locatorGenerators.js';
2
+ import { formatObject } from '../vendor/playwright/stringUtils.js';
3
+ import { PageAliases, isBlankUrl, shortcutOf, signalsOf, toKeyboardModifiers } from '../recording.js';
4
+ import { singleQuote } from './writer.js';
5
+ // Test body written against the Playwright JavaScript API. Used by the Playwright target and by
6
+ // the Selenium JavaScript target, whose support file implements the same API on WebDriver.
7
+ // Follows Playwright's own `playwright-test` code generator.
8
+ export function writeJavaScriptBody(w, recording, options) {
9
+ const aliases = new PageAliases();
10
+ for (const actionInContext of recording.actions) {
11
+ const action = actionInContext.action;
12
+ // Resolve before skipping, so pages are numbered in the order they are opened.
13
+ const pageAlias = aliases.alias(actionInContext.pageGuid);
14
+ if (action.name === 'openPage') {
15
+ if (pageAlias !== 'page')
16
+ w.line(`const ${pageAlias} = await page.context().newPage();`);
17
+ if (!isBlankUrl(action.url))
18
+ w.line(`await ${pageAlias}.goto(${singleQuote(action.url)});`);
19
+ continue;
20
+ }
21
+ // The test runner closes pages itself.
22
+ if (action.name === 'closePage')
23
+ continue;
24
+ const signals = signalsOf(actionInContext);
25
+ if (signals.dialog) {
26
+ w.line(`${pageAlias}.once('dialog', dialog => {`);
27
+ w.indent(() => {
28
+ w.line('console.log(`Dialog message: ${dialog.message()}`);');
29
+ w.line('dialog.dismiss().catch(() => {});');
30
+ });
31
+ w.line('});');
32
+ }
33
+ const popupAlias = signals.popup ? aliases.alias(signals.popup.popupPageGuid) : undefined;
34
+ const download = signals.download && options.supportsDownloads ? `download${signals.download.downloadAlias}` : undefined;
35
+ if (popupAlias)
36
+ w.line(`const ${popupAlias}Promise = ${pageAlias}.waitForEvent('popup');`);
37
+ if (download)
38
+ w.line(`const ${download}Promise = ${pageAlias}.waitForEvent('download');`);
39
+ if (signals.download && !download)
40
+ w.line(options.todo('the next step starts a download, which is not checked here.'));
41
+ w.line(actionCall(pageAlias, actionInContext, options));
42
+ if (popupAlias)
43
+ w.line(`const ${popupAlias} = await ${popupAlias}Promise;`);
44
+ if (download)
45
+ w.line(`const ${download} = await ${download}Promise;`);
46
+ if (recording.header.generateExpectSignal && signals.expect)
47
+ w.line(`await expect(${pageAlias}.${asLocator('javascript', signals.expect.selector)}).toBeVisible();`);
48
+ }
49
+ }
50
+ function actionCall(subject, { action }, options) {
51
+ const locator = 'selector' in action ? `${subject}.${asLocator('javascript', action.selector)}` : subject;
52
+ switch (action.name) {
53
+ case 'click': {
54
+ const method = action.clickCount === 2 ? 'dblclick' : 'click';
55
+ const clickOptions = {};
56
+ if (action.button !== 'left')
57
+ clickOptions.button = action.button;
58
+ const modifiers = toKeyboardModifiers(action.modifiers);
59
+ if (modifiers.length)
60
+ clickOptions.modifiers = modifiers;
61
+ if (action.clickCount > 2)
62
+ clickOptions.clickCount = action.clickCount;
63
+ if (action.position)
64
+ clickOptions.position = action.position;
65
+ return `await ${locator}.${method}(${formatOptions(clickOptions)});`;
66
+ }
67
+ case 'hover':
68
+ return `await ${locator}.hover(${formatOptions({ position: action.position })});`;
69
+ case 'check':
70
+ return `await ${locator}.check();`;
71
+ case 'uncheck':
72
+ return `await ${locator}.uncheck();`;
73
+ case 'fill':
74
+ return `await ${locator}.fill(${singleQuote(action.text)});`;
75
+ case 'setInputFiles':
76
+ return `await ${locator}.setInputFiles(${formatObject(action.files.length === 1 ? action.files[0] : action.files)});`;
77
+ case 'press':
78
+ return `await ${locator}.press(${singleQuote(shortcutOf(action))});`;
79
+ case 'navigate':
80
+ return `await ${subject}.goto(${singleQuote(action.url)});`;
81
+ case 'select':
82
+ return `await ${locator}.selectOption(${formatObject(action.options.length === 1 ? action.options[0] : action.options)});`;
83
+ case 'assertText':
84
+ return `await expect(${locator}).${action.substring ? 'toContainText' : 'toHaveText'}(${singleQuote(action.text)});`;
85
+ case 'assertChecked':
86
+ return `await expect(${locator})${action.checked ? '' : '.not'}.toBeChecked();`;
87
+ case 'assertVisible':
88
+ return `await expect(${locator}).toBeVisible();`;
89
+ case 'assertValue':
90
+ return `await expect(${locator}).${action.value ? `toHaveValue(${singleQuote(action.value)})` : 'toBeEmpty()'};`;
91
+ case 'assertSnapshot':
92
+ if (action.ariaSnapshot)
93
+ return `await expect(${locator}).toMatchAriaSnapshot(${singleQuote(action.ariaSnapshot)});`;
94
+ return `${options.todo('aria snapshot assertion; the recording does not include the snapshot.')}\nawait expect(${locator}).toBeVisible();`;
95
+ default:
96
+ return options.todo(`unsupported recorded action ${JSON.stringify(action)}`);
97
+ }
98
+ }
99
+ function formatOptions(value) {
100
+ return Object.values(value).some(v => v !== undefined) ? formatObject(value) : '';
101
+ }
102
+ //# sourceMappingURL=javascript-body.js.map