@xylentis/testgen 1.1.0 → 1.2.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 +25 -0
- package/README.md +189 -14
- package/dist/checks.js +100 -0
- package/dist/cli.js +93 -22
- package/dist/editing.js +167 -0
- package/dist/generate.js +25 -6
- package/dist/generators/index.js +2 -2
- package/dist/generators/javascript-body.js +91 -13
- package/dist/generators/playwright-go.js +85 -21
- package/dist/generators/playwright-ts.js +44 -7
- package/dist/generators/selenium-java.js +201 -109
- package/dist/generators/selenium-js.js +39 -10
- package/dist/generators/test-data.js +171 -0
- package/dist/locator.js +17 -0
- package/dist/recording.js +60 -2
- package/dist/replay.js +268 -0
- package/dist/secrets.js +169 -0
- package/dist/text.js +28 -0
- package/dist/ui/server.js +220 -21
- package/dist/vendor/playwright/locatorParser.js +235 -0
- package/dist/vendor/playwright/locatorUtils.js +67 -0
- package/docs/testgen-ui.png +0 -0
- package/package.json +4 -2
- package/runtime/playwright-go/testgen_data_test.go +64 -0
- package/runtime/playwright-ts/testgen-data.ts +70 -0
- package/runtime/selenium-java/TestgenSupport.java +213 -29
- package/runtime/selenium-js/testgen-support.js +162 -22
- package/ui/app.js +392 -25
- package/ui/index.html +21 -4
- package/ui/style.css +81 -9
|
@@ -3,113 +3,199 @@ import { camelCase, pascalCase } from '../naming.js';
|
|
|
3
3
|
import { PageAliases, isBlankUrl, shortcutOf, signalsOf, toKeyboardModifiers } from '../recording.js';
|
|
4
4
|
import { readRuntime } from './runtime.js';
|
|
5
5
|
import { seleniumBrowser, seleniumDriverOptions } from './selenium-common.js';
|
|
6
|
+
import { collectTestValues, isEmpty, joinPieces, toCsv } from './test-data.js';
|
|
6
7
|
import { CodeWriter, doubleQuote } from './writer.js';
|
|
7
8
|
const javaKeywords = new Set(('abstract assert boolean break byte case catch char class const continue default do double else enum ' +
|
|
8
9
|
'extends final finally float for goto if implements import instanceof int interface long native new package private protected ' +
|
|
9
10
|
'public return short static strictfp super switch synchronized this throw throws transient try void volatile while var record yield').split(' '));
|
|
10
|
-
|
|
11
|
+
const junit5 = {
|
|
11
12
|
id: 'selenium-java',
|
|
12
13
|
framework: 'Selenium WebDriver + JUnit 5',
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
14
|
+
imports: ['org.junit.jupiter.api.AfterEach', 'org.junit.jupiter.api.BeforeEach'],
|
|
15
|
+
modifier: '',
|
|
16
|
+
beforeEach: '@BeforeEach',
|
|
17
|
+
afterEach: '@AfterEach',
|
|
18
|
+
test: provider => provider
|
|
19
|
+
? { annotations: ['@ParameterizedTest', `@MethodSource(${doubleQuote(provider)})`], imports: ['org.junit.jupiter.params.ParameterizedTest', 'org.junit.jupiter.params.provider.MethodSource'] }
|
|
20
|
+
: { annotations: ['@Test'], imports: ['org.junit.jupiter.api.Test'] },
|
|
21
|
+
logger: { code: 'System.out::println', comment: 'Logs every step and assertion result to the console.', imports: [] },
|
|
22
|
+
};
|
|
23
|
+
const testng = {
|
|
24
|
+
id: 'selenium-java-testng',
|
|
25
|
+
framework: 'Selenium WebDriver + TestNG',
|
|
26
|
+
imports: ['org.testng.annotations.AfterMethod', 'org.testng.annotations.BeforeMethod', 'org.testng.annotations.Test'],
|
|
27
|
+
modifier: 'public ',
|
|
28
|
+
beforeEach: '@BeforeMethod',
|
|
29
|
+
// Quit the driver even when the setup failed or the test was skipped by a group filter.
|
|
30
|
+
afterEach: '@AfterMethod(alwaysRun = true)',
|
|
31
|
+
test: provider => provider
|
|
32
|
+
? { annotations: [`@Test(dataProvider = ${doubleQuote(provider)})`], imports: ['org.testng.annotations.DataProvider'] }
|
|
33
|
+
: { annotations: ['@Test'], imports: [] },
|
|
34
|
+
dataProvider: provider => `@DataProvider(name = ${doubleQuote(provider)})`,
|
|
35
|
+
logger: { code: 'message -> Reporter.log(message, true)', comment: 'Logs every step and assertion result to the TestNG report and the console.', imports: ['org.testng.Reporter'] },
|
|
36
|
+
};
|
|
37
|
+
export const seleniumJava = javaGenerator(junit5);
|
|
38
|
+
export const seleniumJavaTestng = javaGenerator(testng);
|
|
39
|
+
// Names a data column cannot take: they are already used in the test method.
|
|
40
|
+
const reservedName = (name) => javaKeywords.has(name) || /^(driver|page\d*|dialog)$/.test(name);
|
|
41
|
+
function javaGenerator(runner) {
|
|
42
|
+
return {
|
|
43
|
+
id: runner.id,
|
|
44
|
+
framework: runner.framework,
|
|
45
|
+
language: 'Java',
|
|
46
|
+
highlight: 'java',
|
|
47
|
+
features: ['logSteps', 'dataDriven'],
|
|
48
|
+
testFileName: name => `${pascalCase(name)}Test.java`,
|
|
49
|
+
generate(recording, options) {
|
|
50
|
+
const { header } = recording;
|
|
51
|
+
const locators = new JavaLocators();
|
|
52
|
+
const body = new CodeWriter(' ');
|
|
53
|
+
const aliases = new PageAliases();
|
|
54
|
+
const todo = (message) => body.line(`// TODO(testgen): ${message}`);
|
|
55
|
+
const values = collectTestValues(recording, { dataDriven: options.dataDriven, reserved: reservedName });
|
|
56
|
+
recording.actions.forEach((actionInContext, index) => {
|
|
57
|
+
const { action } = actionInContext;
|
|
58
|
+
const pageAlias = aliases.alias(actionInContext.pageGuid);
|
|
59
|
+
if (action.name === 'openPage') {
|
|
60
|
+
if (pageAlias !== 'page')
|
|
61
|
+
body.line(`var ${pageAlias} = page.newPage();`);
|
|
62
|
+
if (!isBlankUrl(action.url))
|
|
63
|
+
body.line(`${pageAlias}.navigate(${doubleQuote(action.url)});`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (action.name === 'closePage') {
|
|
67
|
+
// The driver quits after the test; only pages opened later are closed explicitly.
|
|
68
|
+
if (pageAlias !== 'page')
|
|
69
|
+
body.line(`${pageAlias}.close();`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const signals = signalsOf(actionInContext);
|
|
73
|
+
if (signals.dialog) {
|
|
74
|
+
body.block(`${pageAlias}.onceDialog(dialog -> {`, () => {
|
|
75
|
+
body.line('System.out.println("Dialog message: " + dialog.message());');
|
|
76
|
+
body.line('dialog.dismiss();');
|
|
77
|
+
}, '});');
|
|
78
|
+
}
|
|
79
|
+
if (signals.download)
|
|
80
|
+
todo('the next step starts a download, which Selenium cannot wait for.');
|
|
81
|
+
if (action.name === 'assertSnapshot')
|
|
82
|
+
todo('aria snapshot assertions are not available on Selenium; checking visibility instead.');
|
|
83
|
+
const literals = {
|
|
84
|
+
value: text => joinPieces(values.value(index, text), javaPieces),
|
|
85
|
+
isEmpty: text => isEmpty(values.value(index, text)),
|
|
86
|
+
locator: text => joinPieces(values.locator(index, text), javaPieces),
|
|
87
|
+
};
|
|
88
|
+
const call = actionCall(pageAlias, action, locators, literals);
|
|
89
|
+
if (signals.popup)
|
|
90
|
+
body.line(`var ${aliases.alias(signals.popup.popupPageGuid)} = ${pageAlias}.waitForPopup(() -> ${call});`);
|
|
91
|
+
else
|
|
92
|
+
body.line(`${call};`);
|
|
93
|
+
if (header.generateExpectSignal && signals.expect)
|
|
94
|
+
body.line(`${locators.render(pageAlias, parseLocator(signals.expect.selector), literals.locator)}.shouldBeVisible();`);
|
|
92
95
|
});
|
|
96
|
+
const driverOptions = seleniumDriverOptions(header);
|
|
97
|
+
const className = `${pascalCase(options.name)}Test`;
|
|
98
|
+
const methodName = javaKeywords.has(camelCase(options.name)) ? `test${pascalCase(options.name)}` : camelCase(options.name);
|
|
99
|
+
const packageName = options.packageName;
|
|
100
|
+
const columns = values.columns;
|
|
101
|
+
const dataFile = `${className}.csv`;
|
|
102
|
+
const provider = columns.length ? `${methodName}Data` : undefined;
|
|
103
|
+
const test = runner.test(provider);
|
|
104
|
+
const w = new CodeWriter(' ');
|
|
105
|
+
if (packageName) {
|
|
106
|
+
w.line(`package ${packageName};`);
|
|
107
|
+
w.line();
|
|
108
|
+
}
|
|
109
|
+
if (locators.usesMap)
|
|
110
|
+
w.line('import java.util.Map;');
|
|
111
|
+
if (locators.usesPattern)
|
|
112
|
+
w.line('import java.util.regex.Pattern;');
|
|
113
|
+
const imports = [...runner.imports, ...test.imports, 'org.openqa.selenium.WebDriver', ...(options.logSteps ? runner.logger.imports : [])];
|
|
114
|
+
for (const name of [...new Set(imports)].sort())
|
|
115
|
+
w.line(`import ${name};`);
|
|
93
116
|
w.line();
|
|
94
|
-
w.line('
|
|
95
|
-
w.
|
|
96
|
-
|
|
97
|
-
w.line('
|
|
117
|
+
w.line('// Generated by testgen from a Playwright recording; runs on Selenium WebDriver.');
|
|
118
|
+
w.line('// TestgenSupport.java (same package) provides Playwright-style locators with auto-waiting.');
|
|
119
|
+
w.block(`${runner.modifier}class ${className} {`, () => {
|
|
120
|
+
w.line('private WebDriver driver;');
|
|
121
|
+
w.line('private TestgenSupport.Page page;');
|
|
122
|
+
w.line();
|
|
123
|
+
w.line(runner.beforeEach);
|
|
124
|
+
w.block(`${runner.modifier}void setUp() {`, () => {
|
|
125
|
+
if (driverOptions.unsupported.length)
|
|
126
|
+
w.line(`// TODO(testgen): not applied on Selenium: ${driverOptions.unsupported.join(', ')}.`);
|
|
127
|
+
const args = [doubleQuote(seleniumBrowser(header)), ...driverOptions.args.map(doubleQuote)];
|
|
128
|
+
w.line(`driver = TestgenSupport.createDriver(${args.join(', ')});`);
|
|
129
|
+
if (driverOptions.viewport)
|
|
130
|
+
w.line(`TestgenSupport.setViewportSize(driver, ${driverOptions.viewport.width}, ${driverOptions.viewport.height});`);
|
|
131
|
+
w.line('page = new TestgenSupport.Page(driver);');
|
|
132
|
+
if (options.testIdAttribute !== 'data-testid')
|
|
133
|
+
w.line(`page.setTestIdAttribute(${doubleQuote(options.testIdAttribute)});`);
|
|
134
|
+
if (options.logSteps) {
|
|
135
|
+
w.line(`// ${runner.logger.comment}`);
|
|
136
|
+
w.line(`page.setLogger(${runner.logger.code});`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
w.line();
|
|
140
|
+
w.line(runner.afterEach);
|
|
141
|
+
w.block(`${runner.modifier}void tearDown() {`, () => {
|
|
142
|
+
w.line('if (driver != null)');
|
|
143
|
+
w.line(' driver.quit();');
|
|
144
|
+
});
|
|
145
|
+
w.line();
|
|
146
|
+
if (provider) {
|
|
147
|
+
const resource = (packageName ? packageName.replace(/\./g, '/') + '/' : '') + dataFile;
|
|
148
|
+
w.line(`// Data-driven: the test runs once per row of ${dataFile}, found next to this class on the classpath`);
|
|
149
|
+
w.line(`// (Maven: src/test/resources/${resource}). Its first row holds the recorded values.`);
|
|
150
|
+
if (runner.dataProvider)
|
|
151
|
+
w.line(runner.dataProvider(provider));
|
|
152
|
+
w.block(`${runner.modifier}static Object[][] ${provider}() {`, () => {
|
|
153
|
+
w.line(wrapArguments(`return TestgenSupport.readCsv(`, [`${className}.class`, doubleQuote(dataFile), ...columns.map(c => doubleQuote(c.name))], ');', 8));
|
|
154
|
+
});
|
|
155
|
+
w.line();
|
|
156
|
+
}
|
|
157
|
+
else if (options.dataDriven) {
|
|
158
|
+
w.line('// Data-driven: nothing in the recording is typed, selected or asserted with a value, so there is no data file.');
|
|
159
|
+
}
|
|
160
|
+
for (const annotation of test.annotations)
|
|
161
|
+
w.line(annotation);
|
|
162
|
+
w.block(wrapArguments(`${runner.modifier}void ${methodName}(`, columns.map(c => `String ${c.name}`), ') {', 4), () => w.line(body.toString().trimEnd()));
|
|
98
163
|
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
164
|
+
const support = readRuntime('selenium-java/TestgenSupport.java', engine => engine.replace(/\\/g, '\\\\'))
|
|
165
|
+
.replace('/* @testgen-package */', packageName ? `package ${packageName};` : '');
|
|
166
|
+
const files = [
|
|
167
|
+
{ path: `${className}.java`, content: w.toString() },
|
|
168
|
+
{ path: 'TestgenSupport.java', content: support.replace(/^\n+/, ''), support: true },
|
|
169
|
+
];
|
|
170
|
+
if (provider)
|
|
171
|
+
files.push({ path: dataFile, content: toCsv([columns.map(c => c.name), columns.map(c => c.value)]), data: true });
|
|
172
|
+
return files;
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// `head` + comma-separated `args` + `tail`, continued on lines indented 8 more spaces when a line would
|
|
177
|
+
// pass 120 columns; `indent` is where the first line starts.
|
|
178
|
+
function wrapArguments(head, args, tail, indent) {
|
|
179
|
+
const lines = [head];
|
|
180
|
+
args.forEach((arg, i) => {
|
|
181
|
+
const text = arg + (i < args.length - 1 ? ', ' : tail);
|
|
182
|
+
const current = lines[lines.length - 1];
|
|
183
|
+
if (current !== head && indent + current.length + text.trimEnd().length > 120)
|
|
184
|
+
lines.push(' '.repeat(8) + text);
|
|
185
|
+
else
|
|
186
|
+
lines[lines.length - 1] += text;
|
|
187
|
+
});
|
|
188
|
+
if (!args.length)
|
|
189
|
+
lines[0] += tail;
|
|
190
|
+
return lines.map(line => line.trimEnd()).join('\n');
|
|
191
|
+
}
|
|
192
|
+
const javaPieces = {
|
|
193
|
+
text: doubleQuote,
|
|
194
|
+
column: (column) => column.name,
|
|
195
|
+
secret: (secret) => `TestgenSupport.secret(${doubleQuote(secret.name)})`,
|
|
110
196
|
};
|
|
111
|
-
function actionCall(subject, action, locators) {
|
|
112
|
-
const locator = 'selector' in action ? locators.render(subject, parseLocator(action.selector)) : subject;
|
|
197
|
+
function actionCall(subject, action, locators, literals) {
|
|
198
|
+
const locator = 'selector' in action ? locators.render(subject, parseLocator(action.selector), literals.locator) : subject;
|
|
113
199
|
switch (action.name) {
|
|
114
200
|
case 'click': {
|
|
115
201
|
if (action.clickCount === 2 && action.button === 'left' && !action.modifiers && !action.position)
|
|
@@ -135,7 +221,7 @@ function actionCall(subject, action, locators) {
|
|
|
135
221
|
case 'uncheck':
|
|
136
222
|
return `${locator}.uncheck()`;
|
|
137
223
|
case 'fill':
|
|
138
|
-
return `${locator}.fill(${
|
|
224
|
+
return `${locator}.fill(${literals.value(action.text)})`;
|
|
139
225
|
case 'setInputFiles':
|
|
140
226
|
return `${locator}.setInputFiles(${action.files.map(doubleQuote).join(', ')})`;
|
|
141
227
|
case 'press':
|
|
@@ -143,16 +229,16 @@ function actionCall(subject, action, locators) {
|
|
|
143
229
|
case 'navigate':
|
|
144
230
|
return `${subject}.navigate(${doubleQuote(action.url)})`;
|
|
145
231
|
case 'select':
|
|
146
|
-
return `${locator}.selectOption(${action.options.map(doubleQuote).join(', ')})`;
|
|
232
|
+
return `${locator}.selectOption(${action.options.length === 1 ? literals.value(action.options[0]) : action.options.map(doubleQuote).join(', ')})`;
|
|
147
233
|
case 'assertText':
|
|
148
|
-
return `${locator}.${action.substring ? 'shouldContainText' : 'shouldHaveText'}(${
|
|
234
|
+
return `${locator}.${action.substring ? 'shouldContainText' : 'shouldHaveText'}(${literals.value(action.text)})`;
|
|
149
235
|
case 'assertChecked':
|
|
150
236
|
return `${locator}.${action.checked ? 'shouldBeChecked' : 'shouldNotBeChecked'}()`;
|
|
151
237
|
case 'assertVisible':
|
|
152
238
|
case 'assertSnapshot':
|
|
153
239
|
return `${locator}.shouldBeVisible()`;
|
|
154
240
|
case 'assertValue':
|
|
155
|
-
return action.value ? `${locator}.
|
|
241
|
+
return literals.isEmpty(action.value) ? `${locator}.shouldBeEmpty()` : `${locator}.shouldHaveValue(${literals.value(action.value)})`;
|
|
156
242
|
case 'openPage':
|
|
157
243
|
case 'closePage':
|
|
158
244
|
throw new Error(`${action.name} is handled by the caller`);
|
|
@@ -162,12 +248,18 @@ function actionCall(subject, action, locators) {
|
|
|
162
248
|
class JavaLocators {
|
|
163
249
|
usesMap = false;
|
|
164
250
|
usesPattern = false;
|
|
165
|
-
|
|
251
|
+
_literal = doubleQuote;
|
|
252
|
+
// `literal` writes the text arguments of getBy* and filter calls.
|
|
253
|
+
render(subject, steps, literal = doubleQuote) {
|
|
254
|
+
this._literal = literal;
|
|
255
|
+
return this._render(subject, steps);
|
|
256
|
+
}
|
|
257
|
+
_render(subject, steps) {
|
|
166
258
|
return subject + steps.map(step => '.' + this._step(subject, step)).join('');
|
|
167
259
|
}
|
|
168
260
|
_value(matcher) {
|
|
169
261
|
if (!isRegex(matcher))
|
|
170
|
-
return
|
|
262
|
+
return this._literal(matcher);
|
|
171
263
|
this.usesPattern = true;
|
|
172
264
|
const flags = [['i', 'CASE_INSENSITIVE'], ['m', 'MULTILINE'], ['s', 'DOTALL']]
|
|
173
265
|
.filter(([flag]) => matcher.flags.includes(flag))
|
|
@@ -206,15 +298,15 @@ class JavaLocators {
|
|
|
206
298
|
case 'has-not-text':
|
|
207
299
|
return `filterHasNotText(${this._value(step.body)})`;
|
|
208
300
|
case 'has':
|
|
209
|
-
return `filterHas(${this.
|
|
301
|
+
return `filterHas(${this._render(subject, step.inner)})`;
|
|
210
302
|
case 'hasNot':
|
|
211
|
-
return `filterHasNot(${this.
|
|
303
|
+
return `filterHasNot(${this._render(subject, step.inner)})`;
|
|
212
304
|
case 'and':
|
|
213
|
-
return `and(${this.
|
|
305
|
+
return `and(${this._render(subject, step.inner)})`;
|
|
214
306
|
case 'or':
|
|
215
|
-
return `or(${this.
|
|
307
|
+
return `or(${this._render(subject, step.inner)})`;
|
|
216
308
|
case 'chain':
|
|
217
|
-
return `locator(${this.
|
|
309
|
+
return `locator(${this._render(subject, step.inner)})`;
|
|
218
310
|
case 'test-id':
|
|
219
311
|
return `getByTestId(${this._value(step.body)})`;
|
|
220
312
|
case 'role':
|
|
@@ -242,7 +334,7 @@ class JavaLocators {
|
|
|
242
334
|
if (step.name === undefined && !step.attrs?.length)
|
|
243
335
|
return `getByRole(${role})`;
|
|
244
336
|
if (simpleName)
|
|
245
|
-
return `getByRole(${role}, ${
|
|
337
|
+
return `getByRole(${role}, ${this._literal(step.name)})`;
|
|
246
338
|
const options = [];
|
|
247
339
|
if (step.name !== undefined)
|
|
248
340
|
options.push(`"name", ${this._value(step.name)}`);
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { kebabCase, words } from '../naming.js';
|
|
2
|
-
import { writeJavaScriptBody } from './javascript-body.js';
|
|
2
|
+
import { writeJavaScriptBody, writeReadCsv } from './javascript-body.js';
|
|
3
3
|
import { readRuntime } from './runtime.js';
|
|
4
4
|
import { seleniumBrowser, seleniumDriverOptions } from './selenium-common.js';
|
|
5
|
+
import { collectTestValues, toCsv } from './test-data.js';
|
|
5
6
|
import { CodeWriter, singleQuote } from './writer.js';
|
|
6
7
|
export const seleniumJs = {
|
|
7
8
|
id: 'selenium-js',
|
|
8
9
|
framework: 'Selenium WebDriver + Mocha',
|
|
9
10
|
language: 'JavaScript',
|
|
10
11
|
highlight: 'javascript',
|
|
12
|
+
features: ['logSteps', 'dataDriven'],
|
|
11
13
|
testFileName: name => `${kebabCase(name)}.spec.js`,
|
|
12
14
|
generate(recording, options) {
|
|
13
15
|
const { header } = recording;
|
|
14
|
-
const title =
|
|
16
|
+
const title = words(options.name).join(' ');
|
|
17
|
+
// Data columns are read as row.<name>, so they cannot clash with the test's own names.
|
|
18
|
+
const values = collectTestValues(recording, { dataDriven: options.dataDriven, reserved: () => false });
|
|
19
|
+
const { columns } = values;
|
|
20
|
+
const dataFile = `${kebabCase(options.name)}.csv`;
|
|
15
21
|
const driverOptions = seleniumDriverOptions(header);
|
|
16
22
|
const createOptions = [];
|
|
17
23
|
if (driverOptions.viewport)
|
|
@@ -20,12 +26,24 @@ export const seleniumJs = {
|
|
|
20
26
|
createOptions.push(`args: [${driverOptions.args.map(singleQuote).join(', ')}]`);
|
|
21
27
|
const createArgs = [singleQuote(seleniumBrowser(header)), ...(createOptions.length ? [`{ ${createOptions.join(', ')} }`] : [])];
|
|
22
28
|
const attachArgs = ['driver', ...(options.testIdAttribute !== 'data-testid' ? [`{ testIdAttribute: ${singleQuote(options.testIdAttribute)} }`] : [])];
|
|
29
|
+
const imports = ['createDriver', 'Page', 'expect', ...(columns.length ? ['readCsv'] : []), ...(values.secrets.length ? ['secret'] : [])];
|
|
23
30
|
const w = new CodeWriter(' ');
|
|
31
|
+
const body = () => writeJavaScriptBody(w, recording, {
|
|
32
|
+
supportsDownloads: false,
|
|
33
|
+
todo: message => `// TODO(testgen): ${message}`,
|
|
34
|
+
values,
|
|
35
|
+
});
|
|
24
36
|
w.line('// Generated by testgen from a Playwright recording; runs on Selenium WebDriver.');
|
|
25
37
|
w.line('// testgen-support.js implements the Playwright locator API (getByRole, expect, ...) on top of WebDriver.');
|
|
26
|
-
|
|
38
|
+
if (columns.length)
|
|
39
|
+
w.line(`const path = require('node:path');`);
|
|
40
|
+
w.line(`const { ${imports.join(', ')} } = require('./testgen-support');`);
|
|
27
41
|
w.line();
|
|
28
|
-
|
|
42
|
+
if (columns.length) {
|
|
43
|
+
writeReadCsv(w, dataFile, columns);
|
|
44
|
+
w.line();
|
|
45
|
+
}
|
|
46
|
+
w.block(`describe(${singleQuote(title)}, function () {`, () => {
|
|
29
47
|
w.line('this.timeout(120_000);');
|
|
30
48
|
w.line('let driver;');
|
|
31
49
|
w.line('let page;');
|
|
@@ -35,23 +53,34 @@ export const seleniumJs = {
|
|
|
35
53
|
w.line(`// TODO(testgen): not applied on Selenium: ${driverOptions.unsupported.join(', ')}.`);
|
|
36
54
|
w.line(`driver = await createDriver(${createArgs.join(', ')});`);
|
|
37
55
|
w.line(`page = await Page.attach(${attachArgs.join(', ')});`);
|
|
56
|
+
if (options.logSteps) {
|
|
57
|
+
w.line('// Logs every step and assertion result to the console.');
|
|
58
|
+
w.line('page.setLogger(console.log);');
|
|
59
|
+
}
|
|
38
60
|
}, '});');
|
|
39
61
|
w.line();
|
|
40
62
|
w.block('afterEach(async function () {', () => {
|
|
41
63
|
w.line('await driver?.quit();');
|
|
42
64
|
}, '});');
|
|
43
65
|
w.line();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
todo: message => `// TODO(testgen): ${message}`,
|
|
66
|
+
if (columns.length) {
|
|
67
|
+
w.block('for (const [index, row] of rows.entries()) {', () => {
|
|
68
|
+
w.block(`it(\`${title} (row \${index + 1})\`, async function () {`, body, '});');
|
|
48
69
|
});
|
|
49
|
-
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
if (options.dataDriven)
|
|
73
|
+
w.line('// Data-driven: nothing in the recording is typed, selected or asserted with a value, so there is no data file.');
|
|
74
|
+
w.block(`it(${singleQuote(title)}, async function () {`, body, '});');
|
|
75
|
+
}
|
|
50
76
|
}, '});');
|
|
51
|
-
|
|
77
|
+
const files = [
|
|
52
78
|
{ path: this.testFileName(options.name), content: w.toString() },
|
|
53
79
|
{ path: 'testgen-support.js', content: readRuntime('selenium-js/testgen-support.js'), support: true },
|
|
54
80
|
];
|
|
81
|
+
if (columns.length)
|
|
82
|
+
files.push({ path: dataFile, content: toCsv([columns.map(c => c.name), columns.map(c => c.value)]), data: true });
|
|
83
|
+
return files;
|
|
55
84
|
},
|
|
56
85
|
};
|
|
57
86
|
//# sourceMappingURL=selenium-js.js.map
|