codeceptjs 2.2.0 → 2.2.1

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +15 -22
  3. package/bin/codecept.js +3 -1
  4. package/docs/advanced.md +1 -1
  5. package/docs/angular.md +6 -9
  6. package/docs/basics.md +388 -86
  7. package/docs/bdd.md +4 -3
  8. package/docs/build/Nightmare.js +3 -0
  9. package/docs/build/Polly.js +26 -12
  10. package/docs/build/Puppeteer.js +14 -13
  11. package/docs/build/TestCafe.js +101 -2
  12. package/docs/build/WebDriver.js +53 -52
  13. package/docs/changelog.md +86 -57
  14. package/docs/detox.md +235 -0
  15. package/docs/helpers/Detox.md +579 -0
  16. package/docs/helpers/Polly.md +13 -3
  17. package/docs/helpers/Puppeteer.md +155 -156
  18. package/docs/helpers/TestCafe.md +53 -0
  19. package/docs/helpers/WebDriver.md +209 -204
  20. package/docs/locators.md +2 -0
  21. package/docs/mobile.md +5 -1
  22. package/docs/puppeteer.md +59 -13
  23. package/docs/quickstart.md +47 -12
  24. package/docs/testcafe.md +157 -0
  25. package/docs/webdriver.md +453 -0
  26. package/lib/command/definitions.js +152 -7
  27. package/lib/command/gherkin/snippets.js +19 -8
  28. package/lib/command/init.js +30 -22
  29. package/lib/command/utils.js +1 -1
  30. package/lib/container.js +36 -10
  31. package/lib/data/dataScenarioConfig.js +18 -0
  32. package/lib/helper/Nightmare.js +3 -0
  33. package/lib/helper/Polly.js +26 -12
  34. package/lib/helper/Puppeteer.js +14 -13
  35. package/lib/helper/TestCafe.js +72 -2
  36. package/lib/helper/WebDriver.js +53 -52
  37. package/lib/helper/testcafe/testcafe-utils.js +3 -2
  38. package/lib/interfaces/scenarioConfig.js +2 -2
  39. package/lib/listener/config.js +2 -2
  40. package/lib/plugin/allure.js +3 -0
  41. package/lib/step.js +5 -2
  42. package/lib/ui.js +1 -1
  43. package/lib/utils.js +13 -21
  44. package/package.json +14 -12
@@ -8,6 +8,7 @@ const { Parser } = require('gherkin');
8
8
  const glob = require('glob');
9
9
  const fsPath = require('path');
10
10
  const fs = require('fs');
11
+ const escapeStringRegexp = require('escape-string-regexp');
11
12
 
12
13
  const parser = new Parser();
13
14
  parser.stopAtFirstError = false;
@@ -30,7 +31,7 @@ module.exports = function (genPath, options) {
30
31
  output.error('No gherkin steps defined in config. Exiting');
31
32
  process.exit(1);
32
33
  }
33
- if (!config.gherkin.features) {
34
+ if (!options.feature && !config.gherkin.features) {
34
35
  output.error('No gherkin features defined in config. Exiting');
35
36
  process.exit(1);
36
37
  }
@@ -40,7 +41,7 @@ module.exports = function (genPath, options) {
40
41
  }
41
42
 
42
43
  const files = [];
43
- glob.sync(config.gherkin.features, { cwd: global.codecept_dir }).forEach((file) => {
44
+ glob.sync(options.feature || config.gherkin.features, { cwd: options.feature ? '.' : global.codecept_dir }).forEach((file) => {
44
45
  if (!fsPath.isAbsolute(file)) {
45
46
  file = fsPath.join(global.codecept_dir, file);
46
47
  }
@@ -62,11 +63,21 @@ module.exports = function (genPath, options) {
62
63
  try {
63
64
  matchStep(step.text);
64
65
  } catch (err) {
65
- let stepLine = step.text
66
- .replace(/\"(.*?)\"/g, '{string}')
67
- .replace(/(\d+\.\d+)/, '{float}')
68
- .replace(/ (\d+) /, ' {int} ');
69
- stepLine = Object.assign(stepLine, { type: step.keyword.trim(), location: step.location });
66
+ let stepLine;
67
+ if (/[{}()/]/.test(step.text)) {
68
+ stepLine = escapeStringRegexp(step.text)
69
+ .replace(/\//g, '\\/')
70
+ .replace(/\"(.*?)\"/g, '"(.*?)"')
71
+ .replace(/(\d+\\\.\d+)/, '(\\d+\\.\\d+)')
72
+ .replace(/ (\d+) /, ' (\\d+) ');
73
+ stepLine = Object.assign(stepLine, { type: step.keyword.trim(), location: step.location, regexp: true });
74
+ } else {
75
+ stepLine = step.text
76
+ .replace(/\"(.*?)\"/g, '{string}')
77
+ .replace(/(\d+\.\d+)/, '{float}')
78
+ .replace(/ (\d+) /, ' {int} ');
79
+ stepLine = Object.assign(stepLine, { type: step.keyword.trim(), location: step.location, regexp: false });
80
+ }
70
81
  newSteps.push(stepLine);
71
82
  }
72
83
  }
@@ -99,7 +110,7 @@ module.exports = function (genPath, options) {
99
110
  .filter((value, index, self) => self.indexOf(value) === index)
100
111
  .map((step) => {
101
112
  return `
102
- ${step.type}('${step}', () => {
113
+ ${step.type}(${step.regexp ? '/^' : "'"}${step}${step.regexp ? '$/' : "'"}, () => {
103
114
  // From "${step.file}" ${JSON.stringify(step.location)}
104
115
  throw new Error('Not implemented yet');
105
116
  });`;
@@ -4,7 +4,8 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { fileExists, beautify } = require('../utils');
6
6
  const inquirer = require('inquirer');
7
- const getTestRoot = require('./utils').getTestRoot;
7
+ const { getTestRoot } = require('./utils');
8
+ const generateDefinitions = require('./definitions');
8
9
  const isLocal = require('../utils').installedLocally();
9
10
  const mkdirp = require('mkdirp');
10
11
  const { inspect } = require('util');
@@ -123,7 +124,7 @@ module.exports = function (initPath) {
123
124
  // append file mask to the end of tests
124
125
  if (!config.tests.match(/\*(.*?)$/)) {
125
126
  config.tests = `${config.tests.replace(/\/+$/, '')}/*_test.js`;
126
- console.log(`Adding default test mask: ${config.tests}`);
127
+ console.print(`Adding default test mask: ${config.tests}`);
127
128
  }
128
129
 
129
130
  if (result.translation !== noTranslation) config.translation = result.translation;
@@ -158,42 +159,48 @@ module.exports = function (initPath) {
158
159
  }
159
160
  fs.writeFileSync(stepFile, defaultActor);
160
161
  config.include.I = result.steps_file;
161
- success(`Steps file created at ${stepFile}`);
162
- }
163
-
164
- const tsconfig = {
165
- compilerOption: {
166
- allowJs: true,
167
- },
168
- };
169
- const tsconfigJson = beautify(JSON.stringify(tsconfig));
170
- const tsconfigFile = path.join(testsPath, 'tsconfig.json');
171
- if (fileExists(tsconfigFile)) {
172
- print(`tsconfig.json has already exists at ${tsconfigFile}`);
173
- } else {
174
- fs.writeFileSync(tsconfigFile, tsconfigJson);
175
- success(`TypeScript project configuration file created at ${tsconfigFile}`);
162
+ print(`Steps file created at ${stepFile}`);
176
163
  }
177
164
 
178
165
  fs.writeFileSync(configFile, beautify(`exports.config = ${inspect(config, false, 4, false)}`), 'utf-8');
179
- success(`Config created at ${configFile}`);
166
+ print(`Config created at ${configFile}`);
180
167
 
181
168
  if (config.output) {
182
169
  if (!fileExists(config.output)) {
183
170
  mkdirp.sync(path.join(testsPath, config.output));
184
- success(`Directory for temporary output files created at '${config.output}'`);
171
+ print(`Directory for temporary output files created at '${config.output}'`);
185
172
  } else {
186
173
  print(`Directory for temporary output files is already created at '${config.output}'`);
187
174
  }
188
175
  }
189
- success('Almost done! Create your first test by executing `npx codeceptjs gt` (generate test) command');
176
+
177
+
178
+ const jsconfig = {
179
+ compilerOption: {
180
+ allowJs: true,
181
+ },
182
+ };
183
+ const jsconfigJson = beautify(JSON.stringify(jsconfig));
184
+ const jsconfigFile = path.join(testsPath, 'jsconfig.json');
185
+ if (fileExists(jsconfigFile)) {
186
+ print(`jsconfig.json has already exists at ${jsconfigFile}`);
187
+ } else {
188
+ fs.writeFileSync(jsconfigFile, jsconfigJson);
189
+ print(`Intellisense enabled in ${jsconfigFile}`);
190
+ }
191
+
192
+ generateDefinitions(testsPath, {});
193
+
194
+ print('');
195
+ success(' Almost done! Next step:');
196
+ success(' Create your first test by executing `npx codeceptjs gt` command ');
190
197
 
191
198
  if (packages) {
192
199
  print('\n--');
193
200
  if (isLocal) {
194
- print(`Please install dependent packages locally: ${colors.bold(`npm install --save-dev ${packages.join(' ')}`)}`);
201
+ success(`Please install dependent packages locally: ${colors.bold(`npm install --save-dev ${packages.join(' ')}`)}`);
195
202
  } else {
196
- print(`Please install dependent packages globally: [sudo] ${colors.bold(`npm install -g ${packages.join(' ')}`)}`);
203
+ success(`Please install dependent packages globally: [sudo] ${colors.bold(`npm install -g ${packages.join(' ')}`)}`);
197
204
  }
198
205
  }
199
206
  };
@@ -208,6 +215,7 @@ module.exports = function (initPath) {
208
215
  config.helpers[helperName][configName] = helperResult[key];
209
216
  });
210
217
 
218
+ print('');
211
219
  finish();
212
220
  });
213
221
  });
@@ -11,7 +11,7 @@ module.exports.getConfig = function (configFile) {
11
11
  try {
12
12
  return require('../config').load(configFile);
13
13
  } catch (err) {
14
- fail(err.message);
14
+ fail(err.stack);
15
15
  }
16
16
  };
17
17
 
package/lib/container.js CHANGED
@@ -161,22 +161,22 @@ function createHelpers(config) {
161
161
  }
162
162
 
163
163
  function createSupportObjects(config) {
164
- const objects = container.support = new Proxy({}, {
165
- get(target, key) {
166
- // configured but not in support object, yet: load the module
167
- if (key in config && !(key in target)) target[key] = lazyLoad(key);
168
- return target[key];
169
- },
170
- });
164
+ const objects = {};
165
+
166
+ for (const name in config) {
167
+ objects[name] = {}; // placeholders
168
+ }
171
169
 
172
170
  if (!config.I) {
173
- container.support.I = require('./actor')();
171
+ objects.I = require('./actor')();
174
172
 
175
173
  if (container.translation.I !== 'I') {
176
- container.support[container.translation.I] = container.support.I;
174
+ objects[container.translation.I] = objects.I;
177
175
  }
178
176
  }
179
177
 
178
+ container.support = objects;
179
+
180
180
  function lazyLoad(name) {
181
181
  let newObj = getSupportObject(config, name);
182
182
  try {
@@ -190,6 +190,7 @@ function createSupportObjects(config) {
190
190
  }
191
191
  return newObj;
192
192
  }
193
+
193
194
  const asyncWrapper = function (f) {
194
195
  return function () {
195
196
  return f.apply(this, arguments).catch((e) => {
@@ -209,7 +210,32 @@ function createSupportObjects(config) {
209
210
  });
210
211
  });
211
212
 
212
- return objects;
213
+ return new Proxy({}, {
214
+ has(target, key) {
215
+ return key in config;
216
+ },
217
+ ownKeys() {
218
+ return Reflect.ownKeys(config);
219
+ },
220
+ get(target, key) {
221
+ // configured but not in support object, yet: load the module
222
+ if (key in objects && !(key in target)) {
223
+ // load default I
224
+ if (key in objects && !(key in config)) {
225
+ return target[key] = objects[key];
226
+ }
227
+
228
+ // load new object
229
+ const object = lazyLoad(key);
230
+ // check that object is a real object and not an array
231
+ if (Object.prototype.toString.call(object) === '[object Object]') {
232
+ return target[key] = Object.assign(objects[key], object);
233
+ }
234
+ target[key] = object;
235
+ }
236
+ return target[key];
237
+ },
238
+ });
213
239
  }
214
240
 
215
241
  function createPlugins(config, options = {}) {
@@ -61,6 +61,24 @@ class DataScenarioConfig {
61
61
  this.scenarios.forEach(scenario => scenario.tag(tagName));
62
62
  return this;
63
63
  }
64
+
65
+ /**
66
+ * Pass in additional objects to inject into test
67
+ * @param {*} obj
68
+ */
69
+ inject(obj) {
70
+ this.scenarios.forEach(scenario => scenario.inject(obj));
71
+ return this;
72
+ }
73
+
74
+ /**
75
+ * Dynamically injects dependencies, see https://codecept.io/pageobjects/#dynamic-injection
76
+ * @param {*} dependencies
77
+ */
78
+ injectDependencies(dependencies) {
79
+ this.scenarios.forEach(scenario => scenario.injectDependencies(dependencies));
80
+ return this;
81
+ }
64
82
  }
65
83
 
66
84
  module.exports = DataScenarioConfig;
@@ -81,6 +81,9 @@ class Nightmare extends Helper {
81
81
  static _config() {
82
82
  return [
83
83
  { name: 'url', message: 'Base url of site to be tested', default: 'http://localhost' },
84
+ {
85
+ name: 'show', message: 'Show browser window', default: true, type: 'confirm',
86
+ },
84
87
  ];
85
88
  }
86
89
 
@@ -58,7 +58,7 @@ class Polly extends Helper {
58
58
 
59
59
  // Start mocking network requests/responses
60
60
  async _startMocking(title = 'Test') {
61
- if (!this.helpers && !this.helpers.Puppeteer) {
61
+ if (!(this.helpers && this.helpers.Puppeteer)) {
62
62
  throw new Error('Puppeteer is the only supported helper right now');
63
63
  }
64
64
  await this._connectPuppeteer(title);
@@ -67,20 +67,21 @@ class Polly extends Helper {
67
67
  // Connect Puppeteer helper to mock future requests.
68
68
  async _connectPuppeteer(title) {
69
69
  const adapter = require('@pollyjs/adapter-puppeteer');
70
-
71
70
  PollyJS.register(adapter);
71
+
72
72
  const { page } = this.helpers.Puppeteer;
73
+ if (!page) {
74
+ throw new Error('Looks like, there is no open tab');
75
+ }
73
76
  await page.setRequestInterception(true);
74
77
 
75
78
  this.polly = new PollyJS(title, {
79
+ mode: 'passthrough',
76
80
  adapters: ['puppeteer'],
77
81
  adapterOptions: {
78
82
  puppeteer: { page },
79
83
  },
80
84
  });
81
-
82
- // By default let pass through all network requests
83
- if (this.polly) this.polly.server.any().passthrough();
84
85
  }
85
86
 
86
87
  /**
@@ -90,6 +91,7 @@ class Polly extends Helper {
90
91
  * I.mockRequest('GET', '/api/users', 200);
91
92
  * I.mockRequest('ANY', '/secretsRoutes/*', 403);
92
93
  * I.mockRequest('POST', '/secrets', { secrets: 'fakeSecrets' });
94
+ * I.mockRequest('GET', '/api/users/1', 404, 'User not found');
93
95
  * ```
94
96
  *
95
97
  * Multiple requests
@@ -97,17 +99,27 @@ class Polly extends Helper {
97
99
  * ```js
98
100
  * I.mockRequest('GET', ['/secrets', '/v2/secrets'], 403);
99
101
  * ```
102
+ * @param {string} method request method. Can be `GET`, `POST`, `PUT`, etc or `ANY`.
103
+ * @param {string|array} oneOrMoreUrls url(s) to mock. Can be exact URL, a pattern, or an array of URLs.
104
+ * @param {number|string|object} dataOrStatusCode status code when number provided. A response body otherwise
105
+ * @param {string|object} additionalData response body when a status code is set by previous parameter.
106
+ *
100
107
  */
101
- async mockRequest(method, oneOrMoreUrls, dataOrStatusCode) {
108
+ async mockRequest(method, oneOrMoreUrls, dataOrStatusCode, additionalData = null) {
102
109
  await this._checkAndStartMocking();
110
+ const puppeteerConfigUrl = this.helpers.Puppeteer && this.helpers.Puppeteer.options.url;
111
+
103
112
  const handler = this._getRouteHandler(
104
113
  method,
105
114
  oneOrMoreUrls,
106
- this.options.url,
115
+ this.options.url || puppeteerConfigUrl,
107
116
  );
108
117
 
109
118
  if (typeof dataOrStatusCode === 'number') {
110
119
  const statusCode = dataOrStatusCode;
120
+ if (additionalData) {
121
+ return handler.intercept((_, res) => res.status(statusCode).send(additionalData));
122
+ }
111
123
  return handler.intercept((_, res) => res.sendStatus(statusCode));
112
124
  }
113
125
  const data = dataOrStatusCode;
@@ -148,16 +160,18 @@ class Polly extends Helper {
148
160
  */
149
161
  async stopMocking() {
150
162
  if (!this._checkIfMockingStarted()) return;
151
-
152
163
  await this._disconnectPuppeteer();
153
- await this.polly.flush();
154
- await this.polly.stop();
155
- this.polly = undefined;
164
+
165
+ const { polly } = this;
166
+ if (!polly) return;
167
+ await polly.flush();
168
+ await polly.stop();
169
+ delete this.polly;
156
170
  }
157
171
 
158
172
  async _disconnectPuppeteer() {
159
173
  const { page } = this.helpers.Puppeteer;
160
- await page.setRequestInterception(false);
174
+ if (page) await page.setRequestInterception(false);
161
175
  }
162
176
  }
163
177
 
@@ -57,12 +57,13 @@ const consoleLogStore = new Console();
57
57
  * * `keepCookies`: (optional, default: false) - keep cookies between tests when `restart` is set to false.
58
58
  * * `waitForAction`: (optional) how long to wait after click, doubleClick or PressKey actions in ms. Default: 100.
59
59
  * * `waitForNavigation`: (optional, default: 'load'). When to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle0`, `networkidle2`. See [Puppeteer API](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagewaitfornavigationoptions). Array values are accepted as well.
60
+ * * `pressKeyDelay`: (optional, default: '10'). Delay between key presses in ms. Used when calling Puppeteers page.type(...) in fillField/appendField
60
61
  * * `getPageTimeout` (optional, default: '0') config option to set maximum navigation time in milliseconds.
61
62
  * * `waitForTimeout`: (optional) default wait* timeout in ms. Default: 1000.
62
63
  * * `windowSize`: (optional) default window size. Set a dimension like `640x480`.
63
64
  * * `userAgent`: (optional) user-agent string.
64
65
  * * `manualStart`: (optional, default: false) - do not start browser before a test, start it manually inside a helper with `this.helpers["Puppeteer"]._startBrowser()`.
65
- * * `browser`: (optional, default: chrome) - can be changed to `firefox` when using [puppeteer-firefox]((https://codecept.io/helpers/Puppeteer-firefox)).
66
+ * * `browser`: (optional, default: chrome) - can be changed to `firefox` when using [puppeteer-firefox](https://codecept.io/helpers/Puppeteer-firefox).
66
67
  * * `chrome`: (optional) pass additional [Puppeteer run options](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions).
67
68
  *
68
69
  *
@@ -159,6 +160,7 @@ class Puppeteer extends Helper {
159
160
  browser: 'chrome',
160
161
  waitForAction: 100,
161
162
  waitForTimeout: 1000,
163
+ pressKeyDelay: 10,
162
164
  fullPageScreenshots: false,
163
165
  disableScreenshots: false,
164
166
  uniqueScreenshotNames: false,
@@ -189,6 +191,9 @@ class Puppeteer extends Helper {
189
191
  static _config() {
190
192
  return [
191
193
  { name: 'url', message: 'Base url of site to be tested', default: 'http://localhost' },
194
+ {
195
+ name: 'show', message: 'Show browser window', default: true, type: 'confirm',
196
+ },
192
197
  ];
193
198
  }
194
199
 
@@ -599,7 +604,7 @@ class Puppeteer extends Helper {
599
604
  * {{> scrollPageToTop }}
600
605
  */
601
606
  scrollPageToTop() {
602
- return this.page.evaluate(() => {
607
+ return this.executeScript(() => {
603
608
  window.scrollTo(0, 0);
604
609
  });
605
610
  }
@@ -608,7 +613,7 @@ class Puppeteer extends Helper {
608
613
  * {{> scrollPageToBottom }}
609
614
  */
610
615
  scrollPageToBottom() {
611
- return this.page.evaluate(() => {
616
+ return this.executeScript(() => {
612
617
  const body = document.body;
613
618
  const html = document.documentElement;
614
619
  window.scrollTo(0, Math.max(
@@ -627,20 +632,16 @@ class Puppeteer extends Helper {
627
632
  offsetX = locator;
628
633
  locator = null;
629
634
  }
630
- let x = 0;
631
- let y = 0;
635
+
632
636
  if (locator) {
633
637
  const els = await this._locate(locator);
634
638
  assertElementExists(els, locator, 'Element');
635
639
  await els[0]._scrollIntoViewIfNeeded();
636
640
  const elementCoordinates = await els[0]._clickablePoint();
637
- x = elementCoordinates.x;
638
- y = elementCoordinates.y;
641
+ await this.executeScript((x, y) => window.scrollBy(x, y), elementCoordinates.x + offsetX, elementCoordinates.y + offsetY);
642
+ } else {
643
+ await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY);
639
644
  }
640
-
641
- await this.page.evaluate((x, y) => {
642
- window.scrollTo(x, y);
643
- }, x + offsetX, y + offsetY);
644
645
  return this._waitForAction();
645
646
  }
646
647
 
@@ -1086,7 +1087,7 @@ class Puppeteer extends Helper {
1086
1087
  } else if (editable) {
1087
1088
  await this._evaluateHandeInContext(el => el.innerHTML = '', el);
1088
1089
  }
1089
- await el.type(value.toString(), { delay: 10 });
1090
+ await el.type(value.toString(), { delay: this.options.pressKeyDelay });
1090
1091
  return this._waitForAction();
1091
1092
  }
1092
1093
 
@@ -1107,7 +1108,7 @@ class Puppeteer extends Helper {
1107
1108
  const els = await findFields.call(this, field);
1108
1109
  assertElementExists(els, field, 'Field');
1109
1110
  await els[0].press('End');
1110
- await els[0].type(value, { delay: 10 });
1111
+ await els[0].type(value, { delay: this.options.pressKeyDelay });
1111
1112
  return this._waitForAction();
1112
1113
  }
1113
1114
 
@@ -124,6 +124,10 @@ class TestCafe extends Helper {
124
124
  static _config() {
125
125
  return [
126
126
  { name: 'url', message: 'Base url of site to be tested', default: 'http://localhost' },
127
+ { name: 'browser', message: 'Browser to be used', default: 'chrome' },
128
+ {
129
+ name: 'show', message: 'Show browser window', default: true, type: 'confirm',
130
+ },
127
131
  ];
128
132
  }
129
133
 
@@ -248,7 +252,7 @@ class TestCafe extends Helper {
248
252
  async _withinBegin(locator) {
249
253
  const els = await this._locate(locator);
250
254
  assertElementExists(els, locator);
251
- this.context = els.nth(0);
255
+ this.context = await els.nth(0);
252
256
  }
253
257
 
254
258
  async _withinEnd() {
@@ -636,7 +640,7 @@ class TestCafe extends Helper {
636
640
  const el = await els.nth(0);
637
641
 
638
642
  return this.t
639
- .expect(el.value).eql(value)
643
+ .expect(await el.value).eql(value)
640
644
  .catch(mapError);
641
645
  }
642
646
 
@@ -780,10 +784,76 @@ class TestCafe extends Helper {
780
784
  return ClientFunction(() => document.location.href).with({ boundTestRun: this.t })();
781
785
  }
782
786
 
787
+ /**
788
+ * {{> grabPageScrollPosition }}
789
+ */
790
+ async grabPageScrollPosition() {
791
+ return ClientFunction(() => ({ x: window.pageXOffset, y: window.pageYOffset })).with({ boundTestRun: this.t })();
792
+ }
793
+
794
+ /**
795
+ * {{> scrollPageToTop }}
796
+ */
797
+ scrollPageToTop() {
798
+ return ClientFunction(() => window.scrollTo(0, 0)).with({ boundTestRun: this.t })().catch(mapError);
799
+ }
800
+
801
+ /**
802
+ * {{> scrollPageToBottom }}
803
+ */
804
+ scrollPageToBottom() {
805
+ return ClientFunction(() => {
806
+ const body = document.body;
807
+ const html = document.documentElement;
808
+ window.scrollTo(0, Math.max(
809
+ body.scrollHeight, body.offsetHeight,
810
+ html.clientHeight, html.scrollHeight, html.offsetHeight,
811
+ ));
812
+ }).with({ boundTestRun: this.t })().catch(mapError);
813
+ }
814
+
815
+ /**
816
+ * {{> scrollTo }}
817
+ */
818
+ async scrollTo(locator, offsetX = 0, offsetY = 0) {
819
+ if (typeof locator === 'number' && typeof offsetX === 'number') {
820
+ offsetY = offsetX;
821
+ offsetX = locator;
822
+ locator = null;
823
+ }
824
+
825
+ const scrollBy = ClientFunction((offset) => {
826
+ if (window && window.scrollBy && offset) {
827
+ window.scrollBy(offset.x, offset.y);
828
+ }
829
+ }).with({ boundTestRun: this.t });
830
+
831
+ if (locator) {
832
+ const els = await this._locate(locator);
833
+ assertElementExists(els, locator, 'Element');
834
+ const el = await els.nth(0);
835
+ const x = (await el.offsetLeft) + offsetX;
836
+ const y = (await el.offsetTop) + offsetY;
837
+
838
+ return scrollBy({ x, y }).catch(mapError);
839
+ }
840
+
841
+ const x = offsetX;
842
+ const y = offsetY;
843
+ return scrollBy({ x, y }).catch(mapError);
844
+ }
845
+
783
846
  /**
784
847
  * {{> switchTo }}
785
848
  */
786
849
  async switchTo(locator) {
850
+ if (Number.isInteger(locator)) {
851
+ throw new Error('Not supported switching to iframe by number');
852
+ }
853
+
854
+ if (!locator) {
855
+ return this.t.switchToMainWindow();
856
+ }
787
857
  return this.t.switchToIframe(findElements.call(this, this.context, locator));
788
858
  }
789
859