@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.
@@ -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
- export const seleniumJava = {
11
+ const junit5 = {
11
12
  id: 'selenium-java',
12
13
  framework: 'Selenium WebDriver + JUnit 5',
13
- language: 'Java',
14
- highlight: 'java',
15
- testFileName: name => `${pascalCase(name)}Test.java`,
16
- generate(recording, options) {
17
- const { header } = recording;
18
- const locators = new JavaLocators();
19
- const body = new CodeWriter(' ');
20
- const aliases = new PageAliases();
21
- const todo = (message) => body.line(`// TODO(testgen): ${message}`);
22
- recording.actions.forEach(actionInContext => {
23
- const { action } = actionInContext;
24
- const pageAlias = aliases.alias(actionInContext.pageGuid);
25
- if (action.name === 'openPage') {
26
- if (pageAlias !== 'page')
27
- body.line(`var ${pageAlias} = page.newPage();`);
28
- if (!isBlankUrl(action.url))
29
- body.line(`${pageAlias}.navigate(${doubleQuote(action.url)});`);
30
- return;
31
- }
32
- if (action.name === 'closePage') {
33
- // The driver quits after the test; only pages opened later are closed explicitly.
34
- if (pageAlias !== 'page')
35
- body.line(`${pageAlias}.close();`);
36
- return;
37
- }
38
- const signals = signalsOf(actionInContext);
39
- if (signals.dialog) {
40
- body.block(`${pageAlias}.onceDialog(dialog -> {`, () => {
41
- body.line('System.out.println("Dialog message: " + dialog.message());');
42
- body.line('dialog.dismiss();');
43
- }, '});');
44
- }
45
- if (signals.download)
46
- todo('the next step starts a download, which Selenium cannot wait for.');
47
- if (action.name === 'assertSnapshot')
48
- todo('aria snapshot assertions are not available on Selenium; checking visibility instead.');
49
- const call = actionCall(pageAlias, action, locators);
50
- if (signals.popup)
51
- body.line(`var ${aliases.alias(signals.popup.popupPageGuid)} = ${pageAlias}.waitForPopup(() -> ${call});`);
52
- else
53
- body.line(`${call};`);
54
- if (header.generateExpectSignal && signals.expect)
55
- body.line(`${locators.render(pageAlias, parseLocator(signals.expect.selector))}.shouldBeVisible();`);
56
- });
57
- const driverOptions = seleniumDriverOptions(header);
58
- const className = `${pascalCase(options.name)}Test`;
59
- const methodName = javaKeywords.has(camelCase(options.name)) ? `test${pascalCase(options.name)}` : camelCase(options.name);
60
- const packageName = options.packageName;
61
- const w = new CodeWriter(' ');
62
- if (packageName) {
63
- w.line(`package ${packageName};`);
64
- w.line();
65
- }
66
- if (locators.usesMap)
67
- w.line('import java.util.Map;');
68
- if (locators.usesPattern)
69
- w.line('import java.util.regex.Pattern;');
70
- w.line('import org.junit.jupiter.api.AfterEach;');
71
- w.line('import org.junit.jupiter.api.BeforeEach;');
72
- w.line('import org.junit.jupiter.api.Test;');
73
- w.line('import org.openqa.selenium.WebDriver;');
74
- w.line();
75
- w.line('// Generated by testgen from a Playwright recording; runs on Selenium WebDriver.');
76
- w.line('// TestgenSupport.java (same package) provides Playwright-style locators with auto-waiting.');
77
- w.block(`class ${className} {`, () => {
78
- w.line('private WebDriver driver;');
79
- w.line('private TestgenSupport.Page page;');
80
- w.line();
81
- w.line('@BeforeEach');
82
- w.block('void setUp() {', () => {
83
- if (driverOptions.unsupported.length)
84
- w.line(`// TODO(testgen): not applied on Selenium: ${driverOptions.unsupported.join(', ')}.`);
85
- const args = [doubleQuote(seleniumBrowser(header)), ...driverOptions.args.map(doubleQuote)];
86
- w.line(`driver = TestgenSupport.createDriver(${args.join(', ')});`);
87
- if (driverOptions.viewport)
88
- w.line(`TestgenSupport.setViewportSize(driver, ${driverOptions.viewport.width}, ${driverOptions.viewport.height});`);
89
- w.line('page = new TestgenSupport.Page(driver);');
90
- if (options.testIdAttribute !== 'data-testid')
91
- w.line(`page.setTestIdAttribute(${doubleQuote(options.testIdAttribute)});`);
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('@AfterEach');
95
- w.block('void tearDown() {', () => {
96
- w.line('if (driver != null)');
97
- w.line(' driver.quit();');
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
- w.line();
100
- w.line('@Test');
101
- w.block(`void ${methodName}() {`, () => w.line(body.toString().trimEnd()));
102
- });
103
- const support = readRuntime('selenium-java/TestgenSupport.java', engine => engine.replace(/\\/g, '\\\\'))
104
- .replace('/* @testgen-package */', packageName ? `package ${packageName};` : '');
105
- return [
106
- { path: `${className}.java`, content: w.toString() },
107
- { path: 'TestgenSupport.java', content: support.replace(/^\n+/, ''), support: true },
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(${doubleQuote(action.text)})`;
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'}(${doubleQuote(action.text)})`;
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}.shouldHaveValue(${doubleQuote(action.value)})` : `${locator}.shouldBeEmpty()`;
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
- render(subject, steps) {
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 doubleQuote(matcher);
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.render(subject, step.inner)})`;
301
+ return `filterHas(${this._render(subject, step.inner)})`;
210
302
  case 'hasNot':
211
- return `filterHasNot(${this.render(subject, step.inner)})`;
303
+ return `filterHasNot(${this._render(subject, step.inner)})`;
212
304
  case 'and':
213
- return `and(${this.render(subject, step.inner)})`;
305
+ return `and(${this._render(subject, step.inner)})`;
214
306
  case 'or':
215
- return `or(${this.render(subject, step.inner)})`;
307
+ return `or(${this._render(subject, step.inner)})`;
216
308
  case 'chain':
217
- return `locator(${this.render(subject, step.inner)})`;
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}, ${doubleQuote(step.name)})`;
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 = singleQuote(words(options.name).join(' '));
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
- w.line(`const { createDriver, Page, expect } = require('./testgen-support');`);
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
- w.block(`describe(${title}, function () {`, () => {
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
- w.block(`it(${title}, async function () {`, () => {
45
- writeJavaScriptBody(w, recording, {
46
- supportsDownloads: false,
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
- return [
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