@xylentis/testgen 1.0.0 → 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/CHANGELOG.md +11 -0
- package/README.md +3 -2
- package/dist/cli.js +3 -0
- package/dist/record.js +13 -27
- package/dist/recorder-process.js +102 -0
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.1.0 (2026-10-06)
|
|
4
|
+
|
|
5
|
+
- Khi ghi thao tác không còn mở cửa sổ Playwright Inspector, chỉ còn trình duyệt bạn đang thao tác. Code xem trong giao diện testgen hoặc trong các file sinh ra.
|
|
6
|
+
- Thêm `testgen record --inspector` để hiện lại Inspector khi cần.
|
|
7
|
+
|
|
8
|
+
## 1.0.0 (2026-10-05)
|
|
9
|
+
|
|
10
|
+
- Bản đầu tiên. Ghi thao tác bằng recorder của Playwright, sinh test cho `playwright-ts`, `playwright-go`, `selenium-java`, `selenium-js`.
|
|
11
|
+
- Có giao diện web (`testgen ui`) xem code của mọi target ngay khi đang ghi.
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ testgen ui # mở http://127.0.0.1:9323
|
|
|
39
39
|
1. Nhập URL, chọn trình duyệt rồi bấm **Record**.
|
|
40
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
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
|
|
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
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
44
|
4. Đóng trình duyệt hoặc bấm **Stop** khi xong.
|
|
45
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`.
|
|
@@ -77,6 +77,7 @@ testgen targets # liệt kê target
|
|
|
77
77
|
| `--test-id-attribute` | Thuộc tính dùng cho `getByTestId` (mặc định `data-testid`). |
|
|
78
78
|
| `--save` | (`record`) Nơi lưu bản ghi `.jsonl`. |
|
|
79
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). |
|
|
80
81
|
|
|
81
82
|
## Chạy test đã sinh
|
|
82
83
|
|
|
@@ -129,7 +130,7 @@ Bên dưới vẫn là Selenium WebDriver: các hành động dùng `WebElement.
|
|
|
129
130
|
|
|
130
131
|
```
|
|
131
132
|
Bạn thao tác trên trình duyệt
|
|
132
|
-
│ playwright codegen --target=jsonl
|
|
133
|
+
│ recorder của Playwright (như `playwright codegen --target=jsonl`, Inspector ẩn), ghi lại mỗi bước
|
|
133
134
|
▼
|
|
134
135
|
recording.jsonl ── danh sách bước, chưa gắn với ngôn ngữ nào (click, fill, assertText…)
|
|
135
136
|
│ selector → chuỗi locator: dùng lại mã của Playwright (src/vendor/playwright)
|
package/dist/cli.js
CHANGED
|
@@ -31,6 +31,7 @@ Recorder options (record), same as playwright codegen:
|
|
|
31
31
|
--browser <chromium|firefox|webkit> --channel <chrome|msedge|...> --device <name>
|
|
32
32
|
--viewport-size <w,h> --lang <locale> --timezone <id> --color-scheme <light|dark>
|
|
33
33
|
--user-agent <ua> --ignore-https-errors --load-storage <file> --save-storage <file>
|
|
34
|
+
--inspector also show Playwright's Inspector window (hidden by default)
|
|
34
35
|
`;
|
|
35
36
|
const options = {
|
|
36
37
|
'target': { type: 'string', short: 't' },
|
|
@@ -51,6 +52,7 @@ const options = {
|
|
|
51
52
|
'ignore-https-errors': { type: 'boolean' },
|
|
52
53
|
'load-storage': { type: 'string' },
|
|
53
54
|
'save-storage': { type: 'string' },
|
|
55
|
+
'inspector': { type: 'boolean' },
|
|
54
56
|
'port': { type: 'string', default: '9323' },
|
|
55
57
|
'host': { type: 'string', default: '127.0.0.1' },
|
|
56
58
|
'open': { type: 'boolean', default: true },
|
|
@@ -111,6 +113,7 @@ async function main(argv) {
|
|
|
111
113
|
loadStorage: values['load-storage'],
|
|
112
114
|
saveStorage: values['save-storage'],
|
|
113
115
|
testIdAttribute: values['test-id-attribute'],
|
|
116
|
+
inspector: values.inspector,
|
|
114
117
|
};
|
|
115
118
|
console.log('Recording... interact with the page, then close the browser window (or press Ctrl+C) to generate the tests.');
|
|
116
119
|
const recorder = startCodegen(rest[0], save, codegen);
|
package/dist/record.js
CHANGED
|
@@ -1,32 +1,18 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export function codegenArguments(url, outputFile, options) {
|
|
9
|
-
const args = ['codegen', '--target=jsonl', '--output', outputFile];
|
|
10
|
-
const flags = [
|
|
11
|
-
['browser', '--browser'], ['channel', '--channel'], ['device', '--device'], ['viewportSize', '--viewport-size'],
|
|
12
|
-
['lang', '--lang'], ['timezone', '--timezone'], ['colorScheme', '--color-scheme'], ['userAgent', '--user-agent'],
|
|
13
|
-
['loadStorage', '--load-storage'], ['saveStorage', '--save-storage'], ['testIdAttribute', '--test-id-attribute'],
|
|
14
|
-
];
|
|
15
|
-
for (const [key, flag] of flags) {
|
|
16
|
-
const value = options[key];
|
|
17
|
-
if (typeof value === 'string' && value)
|
|
18
|
-
args.push(flag, value);
|
|
19
|
-
}
|
|
20
|
-
if (options.ignoreHttpsErrors)
|
|
21
|
-
args.push('--ignore-https-errors');
|
|
22
|
-
if (url)
|
|
23
|
-
args.push(url);
|
|
24
|
-
return args;
|
|
25
|
-
}
|
|
26
|
-
// Starts the Playwright recorder; it rewrites `outputFile` (JSONL) after every recorded action
|
|
27
|
-
// and exits when its browser window is closed.
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
// Compiled: dist/recorder-process.js. From sources (tests), Node runs the .ts file directly.
|
|
4
|
+
const recorderProcess = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? './recorder-process.ts' : './recorder-process.js', import.meta.url));
|
|
5
|
+
// Starts the Playwright recorder in a child process. It rewrites `outputFile` (JSONL) after every
|
|
6
|
+
// recorded action and exits when the recorded browser is closed.
|
|
7
|
+
// TESTGEN_RECORDER_HEADLESS=1 records in a headless browser (for tests).
|
|
28
8
|
export function startCodegen(url, outputFile, options, stdio = 'inherit') {
|
|
29
|
-
|
|
9
|
+
const processOptions = {
|
|
10
|
+
...options,
|
|
11
|
+
url,
|
|
12
|
+
outputFile,
|
|
13
|
+
headless: !!process.env.TESTGEN_RECORDER_HEADLESS,
|
|
14
|
+
};
|
|
15
|
+
return spawn(process.execPath, [recorderProcess, JSON.stringify(processOptions)], { stdio });
|
|
30
16
|
}
|
|
31
17
|
export function waitForExit(child) {
|
|
32
18
|
return new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Runs Playwright's recorder (the engine behind `playwright codegen`) in its own process, with the
|
|
2
|
+
// Playwright Inspector window hidden: the user only sees the browser being recorded, and the code
|
|
3
|
+
// is shown by testgen. Mirrors codegen's own browser setup (packages/playwright-core/src/cli).
|
|
4
|
+
//
|
|
5
|
+
// Started by record.ts: node recorder-process.js '<RecorderProcessOptions as JSON>'
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import * as playwright from 'playwright-core';
|
|
10
|
+
async function main(options) {
|
|
11
|
+
// Playwright's own hook for running the recorder's Inspector window headless.
|
|
12
|
+
if (!options.inspector)
|
|
13
|
+
process.env.PWTEST_CLI_HEADLESS = '1';
|
|
14
|
+
const browserType = { chromium: playwright.chromium, firefox: playwright.firefox, webkit: playwright.webkit }[options.browser ?? 'chromium'];
|
|
15
|
+
if (!browserType)
|
|
16
|
+
throw new Error(`Unknown browser "${options.browser}": use chromium, firefox or webkit.`);
|
|
17
|
+
const launchOptions = { headless: !!options.headless, handleSIGINT: false };
|
|
18
|
+
if (options.channel)
|
|
19
|
+
launchOptions.channel = options.channel;
|
|
20
|
+
let contextOptions = {};
|
|
21
|
+
if (options.device) {
|
|
22
|
+
const device = playwright.devices[options.device];
|
|
23
|
+
if (!device)
|
|
24
|
+
throw new Error(`Unknown device "${options.device}".`);
|
|
25
|
+
contextOptions = { ...device };
|
|
26
|
+
}
|
|
27
|
+
// Like codegen: a crisp headed browser, and no touch emulation on WebKit for Linux.
|
|
28
|
+
const headedScaleFactor = os.platform() === 'darwin' ? 2 : 1;
|
|
29
|
+
if (browserType.name() === 'webkit' && process.platform === 'linux') {
|
|
30
|
+
delete contextOptions.hasTouch;
|
|
31
|
+
delete contextOptions.isMobile;
|
|
32
|
+
}
|
|
33
|
+
if (options.viewportSize) {
|
|
34
|
+
const [width, height] = options.viewportSize.split(',').map(Number);
|
|
35
|
+
if (!width || !height)
|
|
36
|
+
throw new Error('Invalid viewport size: use "width,height", for example 1280,720.');
|
|
37
|
+
contextOptions.viewport = { width, height };
|
|
38
|
+
}
|
|
39
|
+
if (options.userAgent)
|
|
40
|
+
contextOptions.userAgent = options.userAgent;
|
|
41
|
+
if (options.lang)
|
|
42
|
+
contextOptions.locale = options.lang;
|
|
43
|
+
if (options.colorScheme)
|
|
44
|
+
contextOptions.colorScheme = options.colorScheme;
|
|
45
|
+
if (options.timezone)
|
|
46
|
+
contextOptions.timezoneId = options.timezone;
|
|
47
|
+
if (options.loadStorage)
|
|
48
|
+
contextOptions.storageState = options.loadStorage;
|
|
49
|
+
if (options.ignoreHttpsErrors)
|
|
50
|
+
contextOptions.ignoreHTTPSErrors = true;
|
|
51
|
+
const browser = await browserType.launch(launchOptions);
|
|
52
|
+
const context = await browser.newContext({ ...contextOptions, ...(options.headless ? {} : { deviceScaleFactor: headedScaleFactor }) });
|
|
53
|
+
let closing = false;
|
|
54
|
+
const close = async (code) => {
|
|
55
|
+
if (closing)
|
|
56
|
+
return;
|
|
57
|
+
closing = true;
|
|
58
|
+
if (options.saveStorage)
|
|
59
|
+
await context.storageState({ path: options.saveStorage }).catch(() => { });
|
|
60
|
+
// The recorder rewrites its output file on a 250ms throttle; let the last write land.
|
|
61
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
62
|
+
await browser.close().catch(() => { });
|
|
63
|
+
process.exit(code);
|
|
64
|
+
};
|
|
65
|
+
// Closing the last tab ends the recording, as with codegen.
|
|
66
|
+
context.on('page', page => page.on('close', () => {
|
|
67
|
+
if (!context.pages().length)
|
|
68
|
+
void close(0);
|
|
69
|
+
}));
|
|
70
|
+
browser.on('disconnected', () => void close(0));
|
|
71
|
+
for (const signal of ['SIGINT', 'SIGTERM'])
|
|
72
|
+
process.on(signal, () => void close(0));
|
|
73
|
+
context.setDefaultTimeout(0);
|
|
74
|
+
context.setDefaultNavigationTimeout(0);
|
|
75
|
+
await context._enableRecorder({
|
|
76
|
+
language: 'jsonl',
|
|
77
|
+
mode: 'recording',
|
|
78
|
+
outputFile: path.resolve(options.outputFile),
|
|
79
|
+
launchOptions: options.channel ? { channel: options.channel } : {},
|
|
80
|
+
contextOptions,
|
|
81
|
+
device: options.device,
|
|
82
|
+
saveStorage: options.saveStorage,
|
|
83
|
+
testIdAttributeName: options.testIdAttribute,
|
|
84
|
+
handleSIGINT: false,
|
|
85
|
+
});
|
|
86
|
+
const page = await context.newPage();
|
|
87
|
+
if (options.url)
|
|
88
|
+
await page.goto(normalizeUrl(options.url)).catch(error => console.error(`testgen: could not open ${options.url}: ${error.message}`));
|
|
89
|
+
}
|
|
90
|
+
// Same rules as codegen: local files become file:// URLs, bare hosts get http://.
|
|
91
|
+
function normalizeUrl(url) {
|
|
92
|
+
if (fs.existsSync(url))
|
|
93
|
+
return 'file://' + path.resolve(url);
|
|
94
|
+
if (!/^(https?:|file:|about:|data:)/.test(url))
|
|
95
|
+
return 'http://' + url;
|
|
96
|
+
return url;
|
|
97
|
+
}
|
|
98
|
+
main(JSON.parse(process.argv[2] ?? '{}')).catch(error => {
|
|
99
|
+
console.error(`testgen: ${error.message}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
});
|
|
102
|
+
//# sourceMappingURL=recorder-process.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xylentis/testgen",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Record a UI test once with the Playwright recorder, then generate it for Playwright (TypeScript, Go) and Selenium (Java, JavaScript).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"playwright",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"runtime",
|
|
26
26
|
"ui",
|
|
27
27
|
"docs/testgen-ui.png",
|
|
28
|
-
"NOTICE"
|
|
28
|
+
"NOTICE",
|
|
29
|
+
"CHANGELOG.md"
|
|
29
30
|
],
|
|
30
31
|
"engines": {
|
|
31
32
|
"node": ">=22"
|