diff --git a/docs/helpers/Appium.md b/docs/helpers/Appium.md index 66d58635b..3cce1dae4 100644 --- a/docs/helpers/Appium.md +++ b/docs/helpers/Appium.md @@ -32,20 +32,20 @@ Launch the daemon: `appium` This helper should be configured in codecept.conf.ts or codecept.conf.js -* `appiumV2`: set this to true if you want to run tests with AppiumV2. See more how to setup [here][3] -* `app`: Application path. Local path or remote URL to an .ipa or .apk file, or a .zip containing one of these. Alias to desiredCapabilities.appPackage -* `host`: (default: 'localhost') Appium host -* `port`: (default: '4723') Appium port -* `platform`: (Android or IOS), which mobile OS to use; alias to desiredCapabilities.platformName -* `restart`: restart browser or app between tests (default: true), if set to false cookies will be cleaned but browser window will be kept and for apps nothing will be changed. -* `desiredCapabilities`: \[], Appium capabilities, see below - * `platformName` - Which mobile OS platform to use - * `appPackage` - Java package of the Android app you want to run - * `appActivity` - Activity name for the Android activity you want to launch from your package. - * `deviceName`: The kind of mobile device or emulator to use - * `platformVersion`: Mobile OS version - * `app` - The absolute local path or remote http URL to an .ipa or .apk file, or a .zip containing one of these. Appium will attempt to install this app binary on the appropriate device first. - * `browserName`: Name of mobile web browser to automate. Should be an empty string if automating an app instead. +- `appiumV2`: set this to true if you want to run tests with AppiumV2. See more how to setup [here][3] +- `app`: Application path. Local path or remote URL to an .ipa or .apk file, or a .zip containing one of these. Alias to desiredCapabilities.appPackage +- `host`: (default: 'localhost') Appium host +- `port`: (default: '4723') Appium port +- `platform`: (Android or IOS), which mobile OS to use; alias to desiredCapabilities.platformName +- `restart`: restart browser or app between tests (default: true), if set to false cookies will be cleaned but browser window will be kept and for apps nothing will be changed. +- `desiredCapabilities`: \[], Appium capabilities, see below + - `platformName` - Which mobile OS platform to use + - `appPackage` - Java package of the Android app you want to run + - `appActivity` - Activity name for the Android activity you want to launch from your package. + - `deviceName`: The kind of mobile device or emulator to use + - `platformVersion`: Mobile OS version + - `app` - The absolute local path or remote http URL to an .ipa or .apk file, or a .zip containing one of these. Appium will attempt to install this app binary on the appropriate device first. + - `browserName`: Name of mobile web browser to automate. Should be an empty string if automating an app instead. Example Android App: @@ -157,7 +157,7 @@ let browser = this.helpers['Appium'].browser ### Parameters -* `config` +- `config` ### runOnIOS @@ -165,35 +165,38 @@ Execute code only on iOS ```js I.runOnIOS(() => { - I.click('//UIAApplication[1]/UIAWindow[1]/UIAButton[1]'); - I.see('Hi, IOS', '~welcome'); -}); + I.click('//UIAApplication[1]/UIAWindow[1]/UIAButton[1]') + I.see('Hi, IOS', '~welcome') +}) ``` Additional filter can be applied by checking for capabilities. For instance, this code will be executed only on iPhone 5s: ```js -I.runOnIOS({deviceName: 'iPhone 5s'},() => { - // ... -}); +I.runOnIOS({ deviceName: 'iPhone 5s' }, () => { + // ... +}) ``` Also capabilities can be checked by a function. ```js -I.runOnAndroid((caps) => { - // caps is current config of desiredCapabiliites - return caps.platformVersion >= 6 -},() => { - // ... -}); +I.runOnAndroid( + caps => { + // caps is current config of desiredCapabiliites + return caps.platformVersion >= 6 + }, + () => { + // ... + }, +) ``` #### Parameters -* `caps` **any** -* `fn` **any** +- `caps` **any** +- `fn` **any** ### runOnAndroid @@ -201,35 +204,38 @@ Execute code only on Android ```js I.runOnAndroid(() => { - I.click('io.selendroid.testapp:id/buttonTest'); -}); + I.click('io.selendroid.testapp:id/buttonTest') +}) ``` Additional filter can be applied by checking for capabilities. For instance, this code will be executed only on Android 6.0: ```js -I.runOnAndroid({platformVersion: '6.0'},() => { - // ... -}); +I.runOnAndroid({ platformVersion: '6.0' }, () => { + // ... +}) ``` Also capabilities can be checked by a function. In this case, code will be executed only on Android >= 6. ```js -I.runOnAndroid((caps) => { - // caps is current config of desiredCapabiliites - return caps.platformVersion >= 6 -},() => { - // ... -}); +I.runOnAndroid( + caps => { + // caps is current config of desiredCapabiliites + return caps.platformVersion >= 6 + }, + () => { + // ... + }, +) ``` #### Parameters -* `caps` **any** -* `fn` **any** +- `caps` **any** +- `fn` **any** ### runInWeb @@ -237,9 +243,9 @@ Execute code only in Web mode. ```js I.runInWeb(() => { - I.waitForElement('#data'); - I.seeInCurrentUrl('/data'); -}); + I.waitForElement('#data') + I.seeInCurrentUrl('/data') +}) ``` ### checkIfAppIsInstalled @@ -247,12 +253,12 @@ I.runInWeb(() => { Returns app installation status. ```js -I.checkIfAppIsInstalled("com.example.android.apis"); +I.checkIfAppIsInstalled('com.example.android.apis') ``` #### Parameters -* `bundleId` **[string][5]** String ID of bundled app +- `bundleId` **[string][5]** String ID of bundled app Returns **[Promise][6]<[boolean][7]>** Appium: support only Android @@ -261,12 +267,12 @@ Returns **[Promise][6]<[boolean][7]>** Appium: support only Android Check if an app is installed. ```js -I.seeAppIsInstalled("com.example.android.apis"); +I.seeAppIsInstalled('com.example.android.apis') ``` #### Parameters -* `bundleId` **[string][5]** String ID of bundled app +- `bundleId` **[string][5]** String ID of bundled app Returns **[Promise][6]\** Appium: support only Android @@ -275,12 +281,12 @@ Returns **[Promise][6]\** Appium: support only Android Check if an app is not installed. ```js -I.seeAppIsNotInstalled("com.example.android.apis"); +I.seeAppIsNotInstalled('com.example.android.apis') ``` #### Parameters -* `bundleId` **[string][5]** String ID of bundled app +- `bundleId` **[string][5]** String ID of bundled app Returns **[Promise][6]\** Appium: support only Android @@ -289,12 +295,12 @@ Returns **[Promise][6]\** Appium: support only Android Install an app on device. ```js -I.installApp('/path/to/file.apk'); +I.installApp('/path/to/file.apk') ``` #### Parameters -* `path` **[string][5]** path to apk file +- `path` **[string][5]** path to apk file Returns **[Promise][6]\** Appium: support only Android @@ -303,22 +309,22 @@ Returns **[Promise][6]\** Appium: support only Android Remove an app from the device. ```js -I.removeApp('appName', 'com.example.android.apis'); +I.removeApp('appName', 'com.example.android.apis') ``` Appium: support only Android #### Parameters -* `appId` **[string][5]** -* `bundleId` **[string][5]?** ID of bundle +- `appId` **[string][5]** +- `bundleId` **[string][5]?** ID of bundle ### resetApp Reset the currently running app for current session. ```js -I.resetApp(); +I.resetApp() ``` ### seeCurrentActivityIs @@ -326,12 +332,12 @@ I.resetApp(); Check current activity on an Android device. ```js -I.seeCurrentActivityIs(".HomeScreenActivity") +I.seeCurrentActivityIs('.HomeScreenActivity') ``` #### Parameters -* `currentActivity` **[string][5]** +- `currentActivity` **[string][5]** Returns **[Promise][6]\** Appium: support only Android @@ -340,7 +346,7 @@ Returns **[Promise][6]\** Appium: support only Android Check whether the device is locked. ```js -I.seeDeviceIsLocked(); +I.seeDeviceIsLocked() ``` Returns **[Promise][6]\** Appium: support only Android @@ -350,7 +356,7 @@ Returns **[Promise][6]\** Appium: support only Android Check whether the device is not locked. ```js -I.seeDeviceIsUnlocked(); +I.seeDeviceIsUnlocked() ``` Returns **[Promise][6]\** Appium: support only Android @@ -360,13 +366,13 @@ Returns **[Promise][6]\** Appium: support only Android Check the device orientation ```js -I.seeOrientationIs('PORTRAIT'); +I.seeOrientationIs('PORTRAIT') I.seeOrientationIs('LANDSCAPE') ``` #### Parameters -* `orientation` **(`"LANDSCAPE"` | `"PORTRAIT"`)** LANDSCAPE or PORTRAITAppium: support Android and iOS +- `orientation` **(`"LANDSCAPE"` | `"PORTRAIT"`)** LANDSCAPE or PORTRAITAppium: support Android and iOS Returns **[Promise][6]\** @@ -375,13 +381,13 @@ Returns **[Promise][6]\** Set a device orientation. Will fail, if app will not set orientation ```js -I.setOrientation('PORTRAIT'); +I.setOrientation('PORTRAIT') I.setOrientation('LANDSCAPE') ``` #### Parameters -* `orientation` **(`"LANDSCAPE"` | `"PORTRAIT"`)** LANDSCAPE or PORTRAITAppium: support Android and iOS +- `orientation` **(`"LANDSCAPE"` | `"PORTRAIT"`)** LANDSCAPE or PORTRAITAppium: support Android and iOS ### grabAllContexts @@ -396,7 +402,7 @@ Returns **[Promise][6]<[Array][8]<[string][5]>>** Appium: support Android and iO Retrieve current context ```js -let context = await I.grabContext(); +let context = await I.grabContext() ``` Returns **[Promise][6]<([string][5] | null)>** Appium: support Android and iOS @@ -406,7 +412,7 @@ Returns **[Promise][6]<([string][5] | null)>** Appium: support Android and iOS Get current device activity. ```js -let activity = await I.grabCurrentActivity(); +let activity = await I.grabCurrentActivity() ``` Returns **[Promise][6]<[string][5]>** Appium: support only Android @@ -418,7 +424,7 @@ The actual server value will be a number. However WebdriverIO additional properties to the response object to allow easier assertions. ```js -let con = await I.grabNetworkConnection(); +let con = await I.grabNetworkConnection() ``` Returns **[Promise][6]<{}>** Appium: support only Android @@ -428,7 +434,7 @@ Returns **[Promise][6]<{}>** Appium: support only Android Get current orientation. ```js -let orientation = await I.grabOrientation(); +let orientation = await I.grabOrientation() ``` Returns **[Promise][6]<[string][5]>** Appium: support Android and iOS @@ -438,7 +444,7 @@ Returns **[Promise][6]<[string][5]>** Appium: support Android and iOS Get all the currently specified settings. ```js -let settings = await I.grabSettings(); +let settings = await I.grabSettings() ``` Returns **[Promise][6]<[string][5]>** Appium: support Android and iOS @@ -449,7 +455,7 @@ Switch to the specified context. #### Parameters -* `context` **any** the context to switch to +- `context` **any** the context to switch to ### switchToWeb @@ -458,33 +464,33 @@ If no context is provided switches to the first detected web context ```js // switch to first web context -I.switchToWeb(); +I.switchToWeb() // or set the context explicitly -I.switchToWeb('WEBVIEW_io.selendroid.testapp'); +I.switchToWeb('WEBVIEW_io.selendroid.testapp') ``` #### Parameters -* `context` **[string][5]?** +- `context` **[string][5]?** Returns **[Promise][6]\** ### switchToNative Switches to native context. -By default switches to NATIVE\_APP context unless other specified. +By default switches to NATIVE_APP context unless other specified. ```js -I.switchToNative(); +I.switchToNative() // or set context explicitly -I.switchToNative('SOME_OTHER_CONTEXT'); +I.switchToNative('SOME_OTHER_CONTEXT') ``` #### Parameters -* `context` **any?** (optional, default `null`) +- `context` **any?** (optional, default `null`) Returns **[Promise][6]\** @@ -493,15 +499,15 @@ Returns **[Promise][6]\** Start an arbitrary Android activity during a session. ```js -I.startActivity('io.selendroid.testapp', '.RegisterUserActivity'); +I.startActivity('io.selendroid.testapp', '.RegisterUserActivity') ``` Appium: support only Android #### Parameters -* `appPackage` **[string][5]** -* `appActivity` **[string][5]** +- `appPackage` **[string][5]** +- `appActivity` **[string][5]** Returns **[Promise][6]\** @@ -509,9 +515,9 @@ Returns **[Promise][6]\** Set network connection mode. -* airplane mode -* wifi mode -* data data +- airplane mode +- wifi mode +- data data ```js I.setNetworkConnection(0) // airplane mode off, wifi off, data off @@ -527,7 +533,7 @@ Appium: support only Android #### Parameters -* `value` **[number][10]** The network connection mode bitmask +- `value` **[number][10]** The network connection mode bitmask Returns **[Promise][6]<[number][10]>** @@ -536,12 +542,12 @@ Returns **[Promise][6]<[number][10]>** Update the current setting on the device ```js -I.setSettings({cyberdelia: 'open'}); +I.setSettings({ cyberdelia: 'open' }) ``` #### Parameters -* `settings` **[object][11]** objectAppium: support Android and iOS +- `settings` **[object][11]** objectAppium: support Android and iOS ### hideDeviceKeyboard @@ -549,7 +555,7 @@ Hide the keyboard. ```js // taps outside to hide keyboard per default -I.hideDeviceKeyboard(); +I.hideDeviceKeyboard() ``` Appium: support Android and iOS @@ -560,12 +566,12 @@ Send a key event to the device. List of keys: [https://developer.android.com/reference/android/view/KeyEvent.html][12] ```js -I.sendDeviceKeyEvent(3); +I.sendDeviceKeyEvent(3) ``` #### Parameters -* `keyValue` **[number][10]** Device specific key value +- `keyValue` **[number][10]** Device specific key value Returns **[Promise][6]\** Appium: support only Android @@ -574,7 +580,7 @@ Returns **[Promise][6]\** Appium: support only Android Open the notifications panel on the device. ```js -I.openNotifications(); +I.openNotifications() ``` Returns **[Promise][6]\** Appium: support only Android @@ -588,13 +594,13 @@ application on the device. [See complete documentation][13] ```js -I.makeTouchAction("~buttonStartWebviewCD", 'tap'); +I.makeTouchAction('~buttonStartWebviewCD', 'tap') ``` #### Parameters -* `locator` -* `action` +- `locator` +- `action` Returns **[Promise][6]\** Appium: support Android and iOS @@ -603,14 +609,14 @@ Returns **[Promise][6]\** Appium: support Android and iOS Taps on element. ```js -I.tap("~buttonStartWebviewCD"); +I.tap('~buttonStartWebviewCD') ``` Shortcut for `makeTouchAction` #### Parameters -* `locator` **any** +- `locator` **any** Returns **[Promise][6]\** @@ -619,18 +625,18 @@ Returns **[Promise][6]\** Perform a swipe on the screen or an element. ```js -let locator = "#io.selendroid.testapp:id/LinearLayout1"; -I.swipe(locator, 800, 1200, 1000); +let locator = '#io.selendroid.testapp:id/LinearLayout1' +I.swipe(locator, 800, 1200, 1000) ``` [See complete reference][14] #### Parameters -* `locator` **([string][5] | [object][11])** -* `xoffset` **[number][10]** -* `yoffset` **[number][10]** -* `speed` **[number][10]** (optional), 1000 by default (optional, default `1000`) +- `locator` **([string][5] | [object][11])** +- `xoffset` **[number][10]** +- `yoffset` **[number][10]** +- `speed` **[number][10]** (optional), 1000 by default (optional, default `1000`) Returns **[Promise][6]\** Appium: support Android and iOS @@ -639,30 +645,30 @@ Returns **[Promise][6]\** Appium: support Android and iOS Perform a swipe on the screen. ```js -I.performSwipe({ x: 300, y: 100 }, { x: 200, y: 100 }); +I.performSwipe({ x: 300, y: 100 }, { x: 200, y: 100 }) ``` #### Parameters -* `from` **[object][11]** -* `to` **[object][11]** Appium: support Android and iOS +- `from` **[object][11]** +- `to` **[object][11]** Appium: support Android and iOS ### swipeDown Perform a swipe down on an element. ```js -let locator = "#io.selendroid.testapp:id/LinearLayout1"; -I.swipeDown(locator); // simple swipe -I.swipeDown(locator, 500); // set speed -I.swipeDown(locator, 1200, 1000); // set offset and speed +let locator = '#io.selendroid.testapp:id/LinearLayout1' +I.swipeDown(locator) // simple swipe +I.swipeDown(locator, 500) // set speed +I.swipeDown(locator, 1200, 1000) // set offset and speed ``` #### Parameters -* `locator` **([string][5] | [object][11])** -* `yoffset` **[number][10]?** (optional) (optional, default `1000`) -* `speed` **[number][10]** (optional), 1000 by default (optional, default `1000`) +- `locator` **([string][5] | [object][11])** +- `yoffset` **[number][10]?** (optional) (optional, default `1000`) +- `speed` **[number][10]** (optional), 1000 by default (optional, default `1000`) Returns **[Promise][6]\** Appium: support Android and iOS @@ -671,17 +677,17 @@ Returns **[Promise][6]\** Appium: support Android and iOS Perform a swipe left on an element. ```js -let locator = "#io.selendroid.testapp:id/LinearLayout1"; -I.swipeLeft(locator); // simple swipe -I.swipeLeft(locator, 500); // set speed -I.swipeLeft(locator, 1200, 1000); // set offset and speed +let locator = '#io.selendroid.testapp:id/LinearLayout1' +I.swipeLeft(locator) // simple swipe +I.swipeLeft(locator, 500) // set speed +I.swipeLeft(locator, 1200, 1000) // set offset and speed ``` #### Parameters -* `locator` **([string][5] | [object][11])** -* `xoffset` **[number][10]?** (optional) (optional, default `1000`) -* `speed` **[number][10]** (optional), 1000 by default (optional, default `1000`) +- `locator` **([string][5] | [object][11])** +- `xoffset` **[number][10]?** (optional) (optional, default `1000`) +- `speed` **[number][10]** (optional), 1000 by default (optional, default `1000`) Returns **[Promise][6]\** Appium: support Android and iOS @@ -690,17 +696,17 @@ Returns **[Promise][6]\** Appium: support Android and iOS Perform a swipe right on an element. ```js -let locator = "#io.selendroid.testapp:id/LinearLayout1"; -I.swipeRight(locator); // simple swipe -I.swipeRight(locator, 500); // set speed -I.swipeRight(locator, 1200, 1000); // set offset and speed +let locator = '#io.selendroid.testapp:id/LinearLayout1' +I.swipeRight(locator) // simple swipe +I.swipeRight(locator, 500) // set speed +I.swipeRight(locator, 1200, 1000) // set offset and speed ``` #### Parameters -* `locator` **([string][5] | [object][11])** -* `xoffset` **[number][10]?** (optional) (optional, default `1000`) -* `speed` **[number][10]** (optional), 1000 by default (optional, default `1000`) +- `locator` **([string][5] | [object][11])** +- `xoffset` **[number][10]?** (optional) (optional, default `1000`) +- `speed` **[number][10]** (optional), 1000 by default (optional, default `1000`) Returns **[Promise][6]\** Appium: support Android and iOS @@ -709,17 +715,17 @@ Returns **[Promise][6]\** Appium: support Android and iOS Perform a swipe up on an element. ```js -let locator = "#io.selendroid.testapp:id/LinearLayout1"; -I.swipeUp(locator); // simple swipe -I.swipeUp(locator, 500); // set speed -I.swipeUp(locator, 1200, 1000); // set offset and speed +let locator = '#io.selendroid.testapp:id/LinearLayout1' +I.swipeUp(locator) // simple swipe +I.swipeUp(locator, 500) // set speed +I.swipeUp(locator, 1200, 1000) // set offset and speed ``` #### Parameters -* `locator` **([string][5] | [object][11])** -* `yoffset` **[number][10]?** (optional) (optional, default `1000`) -* `speed` **[number][10]** (optional), 1000 by default (optional, default `1000`) +- `locator` **([string][5] | [object][11])** +- `yoffset` **[number][10]?** (optional) (optional, default `1000`) +- `speed` **[number][10]** (optional), 1000 by default (optional, default `1000`) Returns **[Promise][6]\** Appium: support Android and iOS @@ -729,22 +735,23 @@ Perform a swipe in selected direction on an element to searchable element. ```js I.swipeTo( - "android.widget.CheckBox", // searchable element - "//android.widget.ScrollView/android.widget.LinearLayout", // scroll element - "up", // direction - 30, - 100, - 500); + 'android.widget.CheckBox', // searchable element + '//android.widget.ScrollView/android.widget.LinearLayout', // scroll element + 'up', // direction + 30, + 100, + 500, +) ``` #### Parameters -* `searchableLocator` **[string][5]** -* `scrollLocator` **[string][5]** -* `direction` **[string][5]** -* `timeout` **[number][10]** -* `offset` **[number][10]** -* `speed` **[number][10]** +- `searchableLocator` **[string][5]** +- `scrollLocator` **[string][5]** +- `direction` **[string][5]** +- `timeout` **[number][10]** +- `offset` **[number][10]** +- `speed` **[number][10]** Returns **[Promise][6]\** Appium: support Android and iOS @@ -754,45 +761,50 @@ Performs a specific touch action. The action object need to contain the action name, x/y coordinates ```js -I.touchPerform([{ +I.touchPerform([ + { action: 'press', options: { x: 100, - y: 200 - } -}, {action: 'release'}]) - -I.touchPerform([{ - action: 'tap', - options: { - element: '1', // json web element was queried before - x: 10, // x offset - y: 20, // y offset - count: 1 // number of touches - } -}]); + y: 200, + }, + }, + { action: 'release' }, +]) + +I.touchPerform([ + { + action: 'tap', + options: { + element: '1', // json web element was queried before + x: 10, // x offset + y: 20, // y offset + count: 1, // number of touches + }, + }, +]) ``` Appium: support Android and iOS #### Parameters -* `actions` **[Array][8]** Array of touch actions +- `actions` **[Array][8]** Array of touch actions ### pullFile Pulls a file from the device. ```js -I.pullFile('/storage/emulated/0/DCIM/logo.png', 'my/path'); +I.pullFile('/storage/emulated/0/DCIM/logo.png', 'my/path') // save file to output dir -I.pullFile('/storage/emulated/0/DCIM/logo.png', output_dir); +I.pullFile('/storage/emulated/0/DCIM/logo.png', output_dir) ``` #### Parameters -* `path` **[string][5]** -* `dest` **[string][5]** +- `path` **[string][5]** +- `dest` **[string][5]** Returns **[Promise][6]<[string][5]>** Appium: support Android and iOS @@ -801,7 +813,7 @@ Returns **[Promise][6]<[string][5]>** Appium: support Android and iOS Perform a shake action on the device. ```js -I.shakeDevice(); +I.shakeDevice() ``` Returns **[Promise][6]\** Appium: support only iOS @@ -818,12 +830,12 @@ See corresponding [webdriverio reference][15]. #### Parameters -* `x` -* `y` -* `duration` -* `radius` -* `rotation` -* `touchCount` +- `x` +- `y` +- `duration` +- `radius` +- `rotation` +- `touchCount` Returns **[Promise][6]\** Appium: support only iOS @@ -835,8 +847,8 @@ See corresponding [webdriverio reference][16]. #### Parameters -* `id` -* `value` +- `id` +- `value` Returns **[Promise][6]\** Appium: support only iOS @@ -845,14 +857,14 @@ Returns **[Promise][6]\** Appium: support only iOS Simulate Touch ID with either valid (match == true) or invalid (match == false) fingerprint. ```js -I.touchId(); // simulates valid fingerprint -I.touchId(true); // simulates valid fingerprint -I.touchId(false); // simulates invalid fingerprint +I.touchId() // simulates valid fingerprint +I.touchId(true) // simulates valid fingerprint +I.touchId(false) // simulates invalid fingerprint ``` #### Parameters -* `match` +- `match` Returns **[Promise][6]\** Appium: support only iOS TODO: not tested @@ -862,7 +874,7 @@ TODO: not tested Close the given application. ```js -I.closeApp(); +I.closeApp() ``` Returns **[Promise][6]\** Appium: support both Android and iOS @@ -873,15 +885,15 @@ Appends text to a input field or textarea. Field is located by name, label, CSS or XPath ```js -I.appendField('#myTextField', 'appended'); +I.appendField('#myTextField', 'appended') // typing secret -I.appendField('password', secret('123456')); +I.appendField('password', secret('123456')) ``` #### Parameters -* `field` **([string][5] | [object][11])** located by label|name|CSS|XPath|strict locator -* `value` **[string][5]** text value to append. +- `field` **([string][5] | [object][11])** located by label|name|CSS|XPath|strict locator +- `value` **[string][5]** text value to append. Returns **void** automatically synchronized promise through #recorder @@ -893,15 +905,15 @@ Element is located by label or name or CSS or XPath. The second parameter is a context (CSS or XPath locator) to narrow the search. ```js -I.checkOption('#agree'); -I.checkOption('I Agree to Terms and Conditions'); -I.checkOption('agree', '//form'); +I.checkOption('#agree') +I.checkOption('I Agree to Terms and Conditions') +I.checkOption('agree', '//form') ``` #### Parameters -* `field` **([string][5] | [object][11])** checkbox located by label | name | CSS | XPath | strict locator. -* `context` **([string][5]? | [object][11])** (optional, `null` by default) element located by CSS | XPath | strict locator. (optional, default `null`) +- `field` **([string][5] | [object][11])** checkbox located by label | name | CSS | XPath | strict locator. +- `context` **([string][5]? | [object][11])** (optional, `null` by default) element located by CSS | XPath | strict locator. (optional, default `null`) Returns **void** automatically synchronized promise through #recorder @@ -916,23 +928,23 @@ The second parameter is a context (CSS or XPath locator) to narrow the search. ```js // simple link -I.click('Logout'); +I.click('Logout') // button of form -I.click('Submit'); +I.click('Submit') // CSS button -I.click('#form input[type=submit]'); +I.click('#form input[type=submit]') // XPath -I.click('//form/*[@type=submit]'); +I.click('//form/*[@type=submit]') // link in context -I.click('Logout', '#nav'); +I.click('Logout', '#nav') // using strict locator -I.click({css: 'nav a.login'}); +I.click({ css: 'nav a.login' }) ``` #### Parameters -* `locator` **([string][5] | [object][11])** clickable link or button located by text, or any element located by CSS|XPath|strict locator. -* `context` **([string][5]? | [object][11] | null)** (optional, `null` by default) element to search in CSS|XPath|Strict locator. (optional, default `null`) +- `locator` **([string][5] | [object][11])** clickable link or button located by text, or any element located by CSS|XPath|strict locator. +- `context` **([string][5]? | [object][11] | null)** (optional, `null` by default) element to search in CSS|XPath|Strict locator. (optional, default `null`) Returns **void** automatically synchronized promise through #recorder @@ -941,14 +953,14 @@ Returns **void** automatically synchronized promise through #recorder Verifies that the specified checkbox is not checked. ```js -I.dontSeeCheckboxIsChecked('#agree'); // located by ID -I.dontSeeCheckboxIsChecked('I agree to terms'); // located by label -I.dontSeeCheckboxIsChecked('agree'); // located by name +I.dontSeeCheckboxIsChecked('#agree') // located by ID +I.dontSeeCheckboxIsChecked('I agree to terms') // located by label +I.dontSeeCheckboxIsChecked('agree') // located by name ``` #### Parameters -* `field` **([string][5] | [object][11])** located by label|name|CSS|XPath|strict locator. +- `field` **([string][5] | [object][11])** located by label|name|CSS|XPath|strict locator. Returns **void** automatically synchronized promise through #recorder @@ -957,12 +969,12 @@ Returns **void** automatically synchronized promise through #recorder Opposite to `seeElement`. Checks that element is not visible (or in DOM) ```js -I.dontSeeElement('.modal'); // modal is not shown +I.dontSeeElement('.modal') // modal is not shown ``` #### Parameters -* `locator` **([string][5] | [object][11])** located by CSS|XPath|Strict locator. +- `locator` **([string][5] | [object][11])** located by CSS|XPath|Strict locator. Returns **void** automatically synchronized promise through #recorder @@ -972,14 +984,14 @@ Checks that value of input field or textarea doesn't equal to given value Opposite to `seeInField`. ```js -I.dontSeeInField('email', 'user@user.com'); // field by name -I.dontSeeInField({ css: 'form input.email' }, 'user@user.com'); // field by CSS +I.dontSeeInField('email', 'user@user.com') // field by name +I.dontSeeInField({ css: 'form input.email' }, 'user@user.com') // field by CSS ``` #### Parameters -* `field` **([string][5] | [object][11])** located by label|name|CSS|XPath|strict locator. -* `value` **([string][5] | [object][11])** value to check. +- `field` **([string][5] | [object][11])** located by label|name|CSS|XPath|strict locator. +- `value` **([string][5] | [object][11])** value to check. Returns **void** automatically synchronized promise through #recorder @@ -989,14 +1001,14 @@ Opposite to `see`. Checks that a text is not present on a page. Use context parameter to narrow down the search. ```js -I.dontSee('Login'); // assume we are already logged in. -I.dontSee('Login', '.nav'); // no login inside .nav element +I.dontSee('Login') // assume we are already logged in. +I.dontSee('Login', '.nav') // no login inside .nav element ``` #### Parameters -* `text` **[string][5]** which is not present. -* `context` **([string][5] | [object][11])?** (optional) element located by CSS|XPath|strict locator in which to perfrom search. (optional, default `null`) +- `text` **[string][5]** which is not present. +- `context` **([string][5] | [object][11])?** (optional) element located by CSS|XPath|strict locator in which to perfrom search. (optional, default `null`) Returns **void** automatically synchronized promise through #recorder @@ -1007,19 +1019,19 @@ Field is located by name, label, CSS, or XPath. ```js // by label -I.fillField('Email', 'hello@world.com'); +I.fillField('Email', 'hello@world.com') // by name -I.fillField('password', secret('123456')); +I.fillField('password', secret('123456')) // by CSS -I.fillField('form#login input[name=username]', 'John'); +I.fillField('form#login input[name=username]', 'John') // or by strict locator -I.fillField({css: 'form#login input[name=username]'}, 'John'); +I.fillField({ css: 'form#login input[name=username]' }, 'John') ``` #### Parameters -* `field` **([string][5] | [object][11])** located by label|name|CSS|XPath|strict locator. -* `value` **([string][5] | [object][11])** text value to fill. +- `field` **([string][5] | [object][11])** located by label|name|CSS|XPath|strict locator. +- `value` **([string][5] | [object][11])** text value to fill. Returns **void** automatically synchronized promise through #recorder @@ -1029,12 +1041,12 @@ Retrieves all texts from an element located by CSS or XPath and returns it to te Resumes test execution, so **should be used inside async with `await`** operator. ```js -let pins = await I.grabTextFromAll('#pin li'); +let pins = await I.grabTextFromAll('#pin li') ``` #### Parameters -* `locator` **([string][5] | [object][11])** element located by CSS|XPath|strict locator. +- `locator` **([string][5] | [object][11])** element located by CSS|XPath|strict locator. Returns **[Promise][6]<[Array][8]<[string][5]>>** attribute value @@ -1044,14 +1056,14 @@ Retrieves a text from an element located by CSS or XPath and returns it to test. Resumes test execution, so **should be used inside async with `await`** operator. ```js -let pin = await I.grabTextFrom('#pin'); +let pin = await I.grabTextFrom('#pin') ``` If multiple elements found returns first element. #### Parameters -* `locator` **([string][5] | [object][11])** element located by CSS|XPath|strict locator. +- `locator` **([string][5] | [object][11])** element located by CSS|XPath|strict locator. Returns **[Promise][6]<[string][5]>** attribute value @@ -1061,12 +1073,12 @@ Grab number of visible elements by locator. Resumes test execution, so **should be used inside async function with `await`** operator. ```js -let numOfElements = await I.grabNumberOfVisibleElements('p'); +let numOfElements = await I.grabNumberOfVisibleElements('p') ``` #### Parameters -* `locator` **([string][5] | [object][11])** located by CSS|XPath|strict locator. +- `locator` **([string][5] | [object][11])** located by CSS|XPath|strict locator. Returns **[Promise][6]<[number][10]>** number of visible elements @@ -1079,13 +1091,13 @@ Resumes test execution, so **should be used inside async with `await`** operator If more than one element is found - attribute of first element is returned. ```js -let hint = await I.grabAttributeFrom('#tooltip', 'title'); +let hint = await I.grabAttributeFrom('#tooltip', 'title') ``` #### Parameters -* `locator` **([string][5] | [object][11])** element located by CSS|XPath|strict locator. -* `attr` **[string][5]** attribute name. +- `locator` **([string][5] | [object][11])** element located by CSS|XPath|strict locator. +- `attr` **[string][5]** attribute name. Returns **[Promise][6]<[string][5]>** attribute value @@ -1096,13 +1108,13 @@ Retrieves an array of attributes from elements located by CSS or XPath and retur Resumes test execution, so **should be used inside async with `await`** operator. ```js -let hints = await I.grabAttributeFromAll('.tooltip', 'title'); +let hints = await I.grabAttributeFromAll('.tooltip', 'title') ``` #### Parameters -* `locator` **([string][5] | [object][11])** element located by CSS|XPath|strict locator. -* `attr` **[string][5]** attribute name. +- `locator` **([string][5] | [object][11])** element located by CSS|XPath|strict locator. +- `attr` **[string][5]** attribute name. Returns **[Promise][6]<[Array][8]<[string][5]>>** attribute value @@ -1112,12 +1124,12 @@ Retrieves an array of value from a form located by CSS or XPath and returns it t Resumes test execution, so **should be used inside async function with `await`** operator. ```js -let inputs = await I.grabValueFromAll('//form/input'); +let inputs = await I.grabValueFromAll('//form/input') ``` #### Parameters -* `locator` **([string][5] | [object][11])** field located by label|name|CSS|XPath|strict locator. +- `locator` **([string][5] | [object][11])** field located by label|name|CSS|XPath|strict locator. Returns **[Promise][6]<[Array][8]<[string][5]>>** attribute value @@ -1128,12 +1140,12 @@ Resumes test execution, so **should be used inside async function with `await`** If more than one element is found - value of first element is returned. ```js -let email = await I.grabValueFrom('input[name=email]'); +let email = await I.grabValueFrom('input[name=email]') ``` #### Parameters -* `locator` **([string][5] | [object][11])** field located by label|name|CSS|XPath|strict locator. +- `locator` **([string][5] | [object][11])** field located by label|name|CSS|XPath|strict locator. Returns **[Promise][6]<[string][5]>** attribute value @@ -1143,12 +1155,12 @@ Saves a screenshot to ouput folder (set in codecept.conf.ts or codecept.conf.js) Filename is relative to output folder. ```js -I.saveScreenshot('debug.png'); +I.saveScreenshot('debug.png') ``` #### Parameters -* `fileName` **[string][5]** file name to save. +- `fileName` **[string][5]** file name to save. Returns **[Promise][6]\** @@ -1157,15 +1169,15 @@ Returns **[Promise][6]\** Scroll element into viewport. ```js -I.scrollIntoView('#submit'); -I.scrollIntoView('#submit', true); -I.scrollIntoView('#submit', { behavior: "smooth", block: "center", inline: "center" }); +I.scrollIntoView('#submit') +I.scrollIntoView('#submit', true) +I.scrollIntoView('#submit', { behavior: 'smooth', block: 'center', inline: 'center' }) ``` #### Parameters -* `locator` **([string][5] | [object][11])** located by CSS|XPath|strict locator. -* `scrollIntoViewOptions` **(ScrollIntoViewOptions | [boolean][7])** either alignToTop=true|false or scrollIntoViewOptions. See [https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView][17]. +- `locator` **([string][5] | [object][11])** located by CSS|XPath|strict locator. +- `scrollIntoViewOptions` **(ScrollIntoViewOptions | [boolean][7])** either alignToTop=true|false or scrollIntoViewOptions. See [https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView][17]. Returns **void** automatically synchronized promise through #recorderSupported only for web testing @@ -1174,14 +1186,14 @@ Returns **void** automatically synchronized promise through #recorderSupported o Verifies that the specified checkbox is checked. ```js -I.seeCheckboxIsChecked('Agree'); -I.seeCheckboxIsChecked('#agree'); // I suppose user agreed to terms -I.seeCheckboxIsChecked({css: '#signup_form input[type=checkbox]'}); +I.seeCheckboxIsChecked('Agree') +I.seeCheckboxIsChecked('#agree') // I suppose user agreed to terms +I.seeCheckboxIsChecked({ css: '#signup_form input[type=checkbox]' }) ``` #### Parameters -* `field` **([string][5] | [object][11])** located by label|name|CSS|XPath|strict locator. +- `field` **([string][5] | [object][11])** located by label|name|CSS|XPath|strict locator. Returns **void** automatically synchronized promise through #recorder @@ -1191,12 +1203,12 @@ Checks that a given Element is visible Element is located by CSS or XPath. ```js -I.seeElement('#modal'); +I.seeElement('#modal') ``` #### Parameters -* `locator` **([string][5] | [object][11])** located by CSS|XPath|strict locator. +- `locator` **([string][5] | [object][11])** located by CSS|XPath|strict locator. Returns **void** automatically synchronized promise through #recorder @@ -1206,16 +1218,16 @@ Checks that the given input field or textarea equals to given value. For fuzzy locators, fields are matched by label text, the "name" attribute, CSS, and XPath. ```js -I.seeInField('Username', 'davert'); -I.seeInField({css: 'form textarea'},'Type your comment here'); -I.seeInField('form input[type=hidden]','hidden_value'); -I.seeInField('#searchform input','Search'); +I.seeInField('Username', 'davert') +I.seeInField({ css: 'form textarea' }, 'Type your comment here') +I.seeInField('form input[type=hidden]', 'hidden_value') +I.seeInField('#searchform input', 'Search') ``` #### Parameters -* `field` **([string][5] | [object][11])** located by label|name|CSS|XPath|strict locator. -* `value` **([string][5] | [object][11])** value to check. +- `field` **([string][5] | [object][11])** located by label|name|CSS|XPath|strict locator. +- `value` **([string][5] | [object][11])** value to check. Returns **void** automatically synchronized promise through #recorder @@ -1225,15 +1237,15 @@ Checks that a page contains a visible text. Use context parameter to narrow down the search. ```js -I.see('Welcome'); // text welcome on a page -I.see('Welcome', '.content'); // text inside .content div -I.see('Register', {css: 'form.register'}); // use strict locator +I.see('Welcome') // text welcome on a page +I.see('Welcome', '.content') // text inside .content div +I.see('Register', { css: 'form.register' }) // use strict locator ``` #### Parameters -* `text` **[string][5]** expected on page. -* `context` **([string][5]? | [object][11])** (optional, `null` by default) element located by CSS|Xpath|strict locator in which to search for text. (optional, default `null`) +- `text` **[string][5]** expected on page. +- `context` **([string][5]? | [object][11])** (optional, `null` by default) element located by CSS|Xpath|strict locator in which to search for text. (optional, default `null`) Returns **void** automatically synchronized promise through #recorder @@ -1244,24 +1256,24 @@ Field is searched by label | name | CSS | XPath. Option is selected by visible text or by value. ```js -I.selectOption('Choose Plan', 'Monthly'); // select by label -I.selectOption('subscription', 'Monthly'); // match option by text -I.selectOption('subscription', '0'); // or by value -I.selectOption('//form/select[@name=account]','Premium'); -I.selectOption('form select[name=account]', 'Premium'); -I.selectOption({css: 'form select[name=account]'}, 'Premium'); +I.selectOption('Choose Plan', 'Monthly') // select by label +I.selectOption('subscription', 'Monthly') // match option by text +I.selectOption('subscription', '0') // or by value +I.selectOption('//form/select[@name=account]', 'Premium') +I.selectOption('form select[name=account]', 'Premium') +I.selectOption({ css: 'form select[name=account]' }, 'Premium') ``` Provide an array for the second argument to select multiple options. ```js -I.selectOption('Which OS do you use?', ['Android', 'iOS']); +I.selectOption('Which OS do you use?', ['Android', 'iOS']) ``` #### Parameters -* `select` **([string][5] | [object][11])** field located by label|name|CSS|XPath|strict locator. -* `option` **([string][5] | [Array][8]\)** visible text or value of option. +- `select` **([string][5] | [object][11])** field located by label|name|CSS|XPath|strict locator. +- `option` **([string][5] | [Array][8]\)** visible text or value of option. Returns **void** automatically synchronized promise through #recorderSupported only for web testing @@ -1271,14 +1283,14 @@ Waits for element to be present on page (by default waits for 1sec). Element can be located by CSS or XPath. ```js -I.waitForElement('.btn.continue'); -I.waitForElement('.btn.continue', 5); // wait for 5 secs +I.waitForElement('.btn.continue') +I.waitForElement('.btn.continue', 5) // wait for 5 secs ``` #### Parameters -* `locator` **([string][5] | [object][11])** element located by CSS|XPath|strict locator. -* `sec` **[number][10]?** (optional, `1` by default) time in seconds to wait (optional, default `null`) +- `locator` **([string][5] | [object][11])** element located by CSS|XPath|strict locator. +- `sec` **[number][10]?** (optional, `1` by default) time in seconds to wait (optional, default `null`) Returns **void** automatically synchronized promise through #recorder @@ -1288,13 +1300,13 @@ Waits for an element to become visible on a page (by default waits for 1sec). Element can be located by CSS or XPath. ```js -I.waitForVisible('#popup'); +I.waitForVisible('#popup') ``` #### Parameters -* `locator` **([string][5] | [object][11])** element located by CSS|XPath|strict locator. -* `sec` **[number][10]** (optional, `1` by default) time in seconds to wait (optional, default `1`) +- `locator` **([string][5] | [object][11])** element located by CSS|XPath|strict locator. +- `sec` **[number][10]** (optional, `1` by default) time in seconds to wait (optional, default `1`) Returns **void** automatically synchronized promise through #recorder @@ -1304,13 +1316,13 @@ Waits for an element to be removed or become invisible on a page (by default wai Element can be located by CSS or XPath. ```js -I.waitForInvisible('#popup'); +I.waitForInvisible('#popup') ``` #### Parameters -* `locator` **([string][5] | [object][11])** element located by CSS|XPath|strict locator. -* `sec` **[number][10]** (optional, `1` by default) time in seconds to wait (optional, default `1`) +- `locator` **([string][5] | [object][11])** element located by CSS|XPath|strict locator. +- `sec` **[number][10]** (optional, `1` by default) time in seconds to wait (optional, default `1`) Returns **void** automatically synchronized promise through #recorder @@ -1321,48 +1333,32 @@ Element can be located by CSS or XPath. Narrow down search results by providing context. ```js -I.waitForText('Thank you, form has been submitted'); -I.waitForText('Thank you, form has been submitted', 5, '#modal'); +I.waitForText('Thank you, form has been submitted') +I.waitForText('Thank you, form has been submitted', 5, '#modal') ``` #### Parameters -* `text` **[string][5]** to wait for. -* `sec` **[number][10]** (optional, `1` by default) time in seconds to wait (optional, default `1`) -* `context` **([string][5] | [object][11])?** (optional) element located by CSS|XPath|strict locator. (optional, default `null`) +- `text` **[string][5]** to wait for. +- `sec` **[number][10]** (optional, `1` by default) time in seconds to wait (optional, default `1`) +- `context` **([string][5] | [object][11])?** (optional) element located by CSS|XPath|strict locator. (optional, default `null`) Returns **void** automatically synchronized promise through #recorder [1]: http://codecept.io/helpers/WebDriver/ - [2]: https://appium.io/docs/en/2.1/ - [3]: https://codecept.io/mobile/#setting-up - [4]: https://github.com/appium/appium/blob/master/packages/appium/docs/en/guides/caps.md - [5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String - [6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise - [7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean - [8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array - [9]: https://webdriver.io/docs/api/chromium/#setnetworkconnection - [10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number - [11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object - [12]: https://developer.android.com/reference/android/view/KeyEvent.html - [13]: http://webdriver.io/api/mobile/touchAction.html - [14]: http://webdriver.io/api/mobile/swipe.html - [15]: http://webdriver.io/api/mobile/rotate.html - [16]: http://webdriver.io/api/mobile/setImmediateValue.html - [17]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView diff --git a/lib/actor.js b/lib/actor.js index 63b5067fe..ff3a54050 100644 --- a/lib/actor.js +++ b/lib/actor.js @@ -1,11 +1,13 @@ -const Step = require('./step'); -const { MetaStep } = require('./step'); -const container = require('./container'); -const { methodsOfObject } = require('./utils'); -const recorder = require('./recorder'); -const event = require('./event'); -const store = require('./store'); -const output = require('./output'); +const Step = require('./step') +const MetaStep = require('./step/meta') +const recordStep = require('./step/record') +const container = require('./container') +const { methodsOfObject } = require('./utils') +const { TIMEOUT_ORDER } = require('./step/timeout') +const recorder = require('./recorder') +const event = require('./event') +const store = require('./store') +const output = require('./output') /** * @interface @@ -21,13 +23,13 @@ class Actor { * ⚠️ returns a promise which is synchronized internally by recorder */ async say(msg, color = 'cyan') { - const step = new Step('say', 'say'); - step.status = 'passed'; + const step = new Step('say', 'say') + step.status = 'passed' return recordStep(step, [msg]).then(() => { // this is backward compatibility as this event may be used somewhere - event.emit(event.step.comment, msg); - output.say(msg, `${color}`); - }); + event.emit(event.step.comment, msg) + output.say(msg, `${color}`) + }) } /** @@ -38,14 +40,16 @@ class Actor { * @inner */ limitTime(timeout) { - if (!store.timeouts) return this; + if (!store.timeouts) return this + + console.log('I.limitTime() is deprecated, use step.timeout() instead') event.dispatcher.prependOnceListener(event.step.before, step => { - output.log(`Timeout to ${step}: ${timeout}s`); - step.setTimeout(timeout * 1000, Step.TIMEOUT_ORDER.codeLimitTime); - }); + output.log(`Timeout to ${step}: ${timeout}s`) + step.setTimeout(timeout * 1000, TIMEOUT_ORDER.codeLimitTime) + }) - return this; + return this } /** @@ -55,11 +59,10 @@ class Actor { * @inner */ retry(opts) { - if (opts === undefined) opts = 1; - recorder.retry(opts); - // remove retry once the step passed - recorder.add(() => event.dispatcher.once(event.step.finished, () => recorder.retries.pop())); - return this; + console.log('I.retry() is deprecated, use step.retry() instead') + const retryStep = require('./step/retry') + retryStep(opts) + return this } } @@ -70,102 +73,54 @@ class Actor { * @ignore */ module.exports = function (obj = {}) { - const actor = container.actor() || new Actor(); + const actor = container.actor() || new Actor() // load all helpers once container initialized container.started(() => { - const translation = container.translation(); - const helpers = container.helpers(); + const translation = container.translation() + const helpers = container.helpers() // add methods from enabled helpers Object.values(helpers).forEach(helper => { methodsOfObject(helper, 'Helper') .filter(method => method !== 'constructor' && method[0] !== '_') .forEach(action => { - const actionAlias = translation.actionAliasFor(action); + const actionAlias = translation.actionAliasFor(action) if (!actor[action]) { actor[action] = actor[actionAlias] = function () { - const step = new Step(helper, action); + const step = new Step(helper, action) if (translation.loaded) { - step.name = actionAlias; - step.actor = translation.I; + step.name = actionAlias + step.actor = translation.I } // add methods to promise chain - return recordStep(step, Array.from(arguments)); - }; + return recordStep(step, Array.from(arguments)) + } } - }); - }); + }) + }) // add translated custom steps from actor Object.keys(obj).forEach(key => { - const actionAlias = translation.actionAliasFor(key); + const actionAlias = translation.actionAliasFor(key) if (!actor[actionAlias]) { - actor[actionAlias] = actor[key]; + actor[actionAlias] = actor[key] } - }); + }) container.append({ support: { I: actor, }, - }); - }); + }) + }) // store.actor = actor; // add custom steps from actor Object.keys(obj).forEach(key => { - const ms = new MetaStep('I', key); - ms.setContext(actor); - actor[key] = ms.run.bind(ms, obj[key]); - }); - - return actor; -}; - -function recordStep(step, args) { - step.status = 'queued'; - step.setArguments(args); - - // run async before step hooks - event.emit(event.step.before, step); - - const task = `${step.name}: ${step.humanizeArgs()}`; - let val; - - // run step inside promise - recorder.add( - task, - () => { - if (!step.startTime) { - // step can be retries - event.emit(event.step.started, step); - step.startTime = Date.now(); - } - return (val = step.run(...args)); - }, - false, - undefined, - step.getTimeout(), - ); - - event.emit(event.step.after, step); - - recorder.add('step passed', () => { - step.endTime = Date.now(); - event.emit(event.step.passed, step, val); - event.emit(event.step.finished, step); - }); - - recorder.catchWithoutStop(err => { - step.status = 'failed'; - step.endTime = Date.now(); - event.emit(event.step.failed, step); - event.emit(event.step.finished, step); - throw err; - }); - - recorder.add('return result', () => val); - // run async after step hooks + const ms = new MetaStep('I', key) + ms.setContext(actor) + actor[key] = ms.run.bind(ms, obj[key]) + }) - return recorder.promise(); + return actor } diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index f074c710f..01e8bcc40 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -3499,6 +3499,11 @@ async function proceedSee(assertType, text, context, strict = false) { allText = await Promise.all(els.map(el => el.innerText())) } + if (store?.currentStep?.opts?.ignoreCase === true) { + text = text.toLowerCase() + allText = allText.map(elText => elText.toLowerCase()) + } + if (strict) { return allText.map(elText => equals(description)[assertType](text, elText)) } diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index ce65db079..3ea66872d 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -2769,6 +2769,11 @@ async function proceedSee(assertType, text, context, strict = false) { allText = await Promise.all(els.map(el => el.getProperty('innerText').then(p => p.jsonValue()))) } + if (store?.currentStep?.opts?.ignoreCase === true) { + text = text.toLowerCase() + allText = allText.map(elText => elText.toLowerCase()) + } + if (strict) { return allText.map(elText => equals(description)[assertType](text, elText)) } diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index c0e5bcaa5..e55b51cf8 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -7,6 +7,7 @@ const Helper = require('@codeceptjs/helper') const promiseRetry = require('promise-retry') const stringIncludes = require('../assert/include').includes const { urlEquals, equals } = require('../assert/equal') +const store = require('../store') const { debug } = require('../output') const { empty } = require('../assert/empty') const { truth } = require('../assert/truth') @@ -2698,7 +2699,14 @@ async function proceedSee(assertType, text, context, strict = false) { const smartWaitEnabled = assertType === 'assert' const res = await this._locate(withStrictLocator(context), smartWaitEnabled) assertElementExists(res, context) - const selected = await forEachAsync(res, async el => this.browser.getElementText(getElementId(el))) + let selected = await forEachAsync(res, async el => this.browser.getElementText(getElementId(el))) + + // apply ignoreCase option + if (store?.currentStep?.opts?.ignoreCase === true) { + text = text.toLowerCase() + selected = selected.map(elText => elText.toLowerCase()) + } + if (strict) { if (Array.isArray(selected) && selected.length !== 0) { return selected.map(elText => equals(description)[assertType](text, elText)) diff --git a/lib/listener/globalTimeout.js b/lib/listener/globalTimeout.js index 0aad80c9f..fc77decc5 100644 --- a/lib/listener/globalTimeout.js +++ b/lib/listener/globalTimeout.js @@ -3,7 +3,7 @@ const output = require('../output') const recorder = require('../recorder') const Config = require('../config') const { timeouts } = require('../store') -const TIMEOUT_ORDER = require('../step').TIMEOUT_ORDER +const { TIMEOUT_ORDER } = require('../step/timeout') module.exports = function () { let timeout diff --git a/lib/listener/steps.js b/lib/listener/steps.js index 38d166c0b..aa33d1e14 100644 --- a/lib/listener/steps.js +++ b/lib/listener/steps.js @@ -73,6 +73,7 @@ module.exports = function () { event.dispatcher.on(event.step.started, step => { step.startedAt = +new Date() step.test = currentTest + store.currentStep = step if (currentHook && Array.isArray(currentHook.steps)) { return currentHook.steps.push(step) } @@ -84,5 +85,7 @@ module.exports = function () { step.finishedAt = +new Date() if (step.startedAt) step.duration = step.finishedAt - step.startedAt debug(`Step '${step}' finished; Duration: ${step.duration || 0}ms`) + store.currentStep = null + store.stepOptions = null }) } diff --git a/lib/plugin/stepTimeout.js b/lib/plugin/stepTimeout.js index 27ed98a94..36f06d5c1 100644 --- a/lib/plugin/stepTimeout.js +++ b/lib/plugin/stepTimeout.js @@ -1,5 +1,5 @@ const event = require('../event') -const TIMEOUT_ORDER = require('../step').TIMEOUT_ORDER +const { TIMEOUT_ORDER } = require('../step/timeout') const defaultConfig = { timeout: 150, diff --git a/lib/recorder.js b/lib/recorder.js index 40db146c7..fa60727d5 100644 --- a/lib/recorder.js +++ b/lib/recorder.js @@ -179,7 +179,7 @@ module.exports = { } if (retry === undefined) retry = true if (!running && !force) { - return + return Promise.resolve() } tasks.push(taskName) debug(chalk.gray(`${currentQueue()} Queued | ${taskName}`)) diff --git a/lib/step.js b/lib/step.js index 51f1ff49b..9295a0835 100644 --- a/lib/step.js +++ b/lib/step.js @@ -1,353 +1,20 @@ -// TODO: place MetaStep in other file, disable rule - -const color = require('chalk') -const store = require('./store') -const Secret = require('./secret') -const event = require('./event') -const { ucfirst } = require('./utils') - -const STACK_LINE = 4 - +// refactored step class, moved to helper /** - * Each command in test executed through `I.` object is wrapped in Step. - * Step allows logging executed commands and triggers hook before and after step execution. - * @param {CodeceptJS.Helper} helper - * @param {string} name + * Step is wrapper around a helper method. + * It is used to create a new step that is a combination of other steps. */ -class Step { - static get TIMEOUT_ORDER() { - return { - /** - * timeouts set with order below zero only override timeouts of higher order if their value is smaller - */ - testOrSuite: -5, - /** - * 0-9 - designated for override of timeouts set from code, 5 is used by stepTimeout plugin when stepTimeout.config.overrideStepLimits=true - */ - stepTimeoutHard: 5, - /** - * 10-19 - designated for timeouts set from code, 15 is order of I.setTimeout(t) operation - */ - codeLimitTime: 15, - /** - * 20-29 - designated for timeout settings which could be overriden in tests code, 25 is used by stepTimeout plugin when stepTimeout.config.overrideStepLimits=false - */ - stepTimeoutSoft: 25, - } - } - - constructor(helper, name) { - /** @member {string} */ - this.actor = 'I' // I = actor - /** @member {CodeceptJS.Helper} */ - this.helper = helper // corresponding helper - /** @member {string} */ - this.name = name // name of a step console - /** @member {string} */ - this.helperMethod = name // helper method - /** @member {string} */ - this.status = 'pending' - /** - * @member {string} suffix - * @memberof CodeceptJS.Step# - */ - /** @member {string} */ - this.prefix = this.suffix = '' - /** @member {string} */ - this.comment = '' - /** @member {Array<*>} */ - this.args = [] - /** @member {MetaStep} */ - this.metaStep = undefined - /** @member {string} */ - this.stack = '' - - const timeouts = new Map() - /** - * @method - * @returns {number|undefined} - */ - this.getTimeout = function () { - let totalTimeout - // iterate over all timeouts starting from highest values of order - new Map([...timeouts.entries()].sort().reverse()).forEach((timeout, order) => { - if ( - timeout !== undefined && - // when orders >= 0 - timeout value overrides those set with higher order elements - (order >= 0 || - // when `order < 0 && totalTimeout === undefined` - timeout is used when nothing is set by elements with higher order - totalTimeout === undefined || - // when `order < 0` - timeout overrides higher values of timeout or 'no timeout' (totalTimeout === 0) set by elements with higher order - (timeout > 0 && (timeout < totalTimeout || totalTimeout === 0))) - ) { - totalTimeout = timeout - } - }) - return totalTimeout - } - /** - * @method - * @param {number} timeout - timeout in milliseconds or 0 if no timeout - * @param {number} order - order defines the priority of timeout, timeouts set with lower order override those set with higher order. - * When order below 0 value of timeout only override if new value is lower - */ - this.setTimeout = function (timeout, order) { - timeouts.set(order, timeout) - } - - this.setTrace() - } - - /** @function */ - setTrace() { - Error.captureStackTrace(this) - } - - /** @param {Array<*>} args */ - setArguments(args) { - this.args = args - } - - /** - * @param {...any} args - * @return {*} - */ - run() { - this.args = Array.prototype.slice.call(arguments) - if (store.dryRun) { - this.setStatus('success') - return Promise.resolve(new Proxy({}, dryRunResolver())) - } - let result - try { - if (this.helperMethod !== 'say') { - result = this.helper[this.helperMethod].apply(this.helper, this.args) - } - this.setStatus('success') - } catch (err) { - this.setStatus('failed') - throw err - } - return result - } - - setActor(actor) { - this.actor = actor || '' - } - - /** @param {string} status */ - setStatus(status) { - this.status = status - if (this.metaStep) { - this.metaStep.setStatus(status) - } - } - - /** @return {string} */ - humanize() { - return humanizeString(this.name) - } - - /** @return {string} */ - humanizeArgs() { - return this.args - .map(arg => { - if (!arg) { - return '' - } - if (typeof arg === 'string') { - return `"${arg}"` - } - if (Array.isArray(arg)) { - try { - const res = JSON.stringify(arg) - return res - } catch (err) { - return `[${arg.toString()}]` - } - } else if (typeof arg === 'function') { - return arg.toString() - } else if (typeof arg === 'undefined') { - return `${arg}` - } else if (arg instanceof Secret) { - return arg.getMasked() - } else if (arg.toString && arg.toString() !== '[object Object]') { - return arg.toString() - } else if (typeof arg === 'object') { - const returnedArg = {} - for (const [key, value] of Object.entries(arg)) { - returnedArg[key] = value - if (value instanceof Secret) returnedArg[key] = value.getMasked() - } - return JSON.stringify(returnedArg) - } - return arg - }) - .join(', ') - } - - /** @return {string} */ - line() { - const lines = this.stack.split('\n') - if (lines[STACK_LINE]) { - return lines[STACK_LINE].trim() - .replace(global.codecept_dir || '', '.') - .trim() - } - return '' - } - - /** @return {string} */ - toString() { - return ucfirst(`${this.prefix}${this.actor} ${this.humanize()} ${this.humanizeArgs()}${this.suffix}`).trim() - } - - /** @return {string} */ - toCliStyled() { - return `${this.prefix}${this.actor} ${color.italic(this.humanize())} ${color.yellow(this.humanizeArgs())}${this.suffix}` - } - - /** @return {string} */ - toCode() { - return `${this.prefix}${this.actor}.${this.name}(${this.humanizeArgs()})${this.suffix}` - } - - isMetaStep() { - return this.constructor.name === 'MetaStep' - } - - /** @return {boolean} */ - hasBDDAncestor() { - let hasBDD = false - let processingStep - processingStep = this - - while (processingStep.metaStep) { - if (processingStep.metaStep.actor.match(/^(Given|When|Then|And)/)) { - hasBDD = true - break - } else { - processingStep = processingStep.metaStep - } - } - return hasBDD - } -} +const BaseStep = require('./step/base') +const StepConfig = require('./step/config') +const Step = require('./step/helper') -/** @extends Step */ -class MetaStep extends Step { - constructor(obj, method) { - if (!method) method = '' - super(null, method) - this.actor = obj - } - - /** @return {boolean} */ - isBDD() { - if (this.actor && this.actor.match && this.actor.match(/^(Given|When|Then|And)/)) { - return true - } - return false - } - - toCliStyled() { - return this.toString() - } - - toString() { - const actorText = this.actor - - if (this.isBDD()) { - return `${this.prefix}${actorText} ${this.name} "${this.humanizeArgs()}${this.suffix}"` - } - - if (actorText === 'I') { - return `${this.prefix}${actorText} ${this.humanize()} ${this.humanizeArgs()}${this.suffix}` - } - - return `On ${this.prefix}${actorText}: ${this.humanize()} ${this.humanizeArgs()}${this.suffix}` - } - - humanize() { - return humanizeString(this.name) - } - - setTrace() {} - - setContext(context) { - this.context = context - } - - /** @return {*} */ - run(fn) { - this.status = 'queued' - this.setArguments(Array.from(arguments).slice(1)) - let result - - const registerStep = step => { - this.metaStep = null - step.metaStep = this - } - event.dispatcher.prependListener(event.step.before, registerStep) - // Handle async and sync methods. - if (fn.constructor.name === 'AsyncFunction') { - result = fn - .apply(this.context, this.args) - .then(result => { - return result - }) - .catch(error => { - this.setStatus('failed') - throw error - }) - .finally(() => { - this.endTime = Date.now() - event.dispatcher.removeListener(event.step.before, registerStep) - }) - } else { - try { - this.startTime = Date.now() - result = fn.apply(this.context, this.args) - } catch (error) { - this.setStatus('failed') - throw error - } finally { - this.endTime = Date.now() - event.dispatcher.removeListener(event.step.before, registerStep) - } - } - - return result - } -} - -Step.TIMEOUTS = {} - -/** @type {Class} */ -Step.MetaStep = MetaStep +/** + * MetaStep is a step that is used to wrap other steps. + * It is used to create a new step that is a combination of other steps. + * It is used to create a new step that is a combination of other steps. + */ +const MetaStep = require('./step/meta') module.exports = Step - -function dryRunResolver() { - return { - get(target, prop) { - if (prop === 'toString') return () => '' - return new Proxy({}, dryRunResolver()) - }, - } -} - -function humanizeString(string) { - // split strings by words, then make them all lowercase - const _result = string - .replace(/([a-z](?=[A-Z]))/g, '$1 ') - .split(' ') - .map(word => word.toLowerCase()) - - _result[0] = _result[0] === 'i' ? capitalizeFLetter(_result[0]) : _result[0] - return _result.join(' ').trim() -} - -function capitalizeFLetter(string) { - return string[0].toUpperCase() + string.slice(1) -} +module.exports.MetaStep = MetaStep +module.exports.BaseStep = BaseStep +module.exports.StepConfig = StepConfig diff --git a/lib/step/base.js b/lib/step/base.js new file mode 100644 index 000000000..47bbe4f5c --- /dev/null +++ b/lib/step/base.js @@ -0,0 +1,180 @@ +const color = require('chalk') +const Secret = require('../secret') +const { getCurrentTimeout } = require('./timeout') +const { ucfirst, humanizeString } = require('../utils') + +const STACK_LINE = 4 + +/** + * Each command in test executed through `I.` object is wrapped in Step. + * Step allows logging executed commands and triggers hook before and after step execution. + * @param {string} name + */ +class Step { + constructor(name) { + /** @member {string} */ + this.name = name + /** @member {Map} */ + this.timeouts = new Map() + + /** @member {Array<*>} */ + this.args = [] + + /** @member {Record} */ + this.opts = {} + /** @member {string} */ + this.actor = 'I' // I = actor + /** @member {string} */ + this.helperMethod = name // helper method + /** @member {string} */ + this.status = 'pending' + /** @member {string} */ + this.prefix = this.suffix = '' + /** @member {string} */ + this.comment = '' + /** @member {any} */ + this.metaStep = undefined + /** @member {string} */ + this.stack = '' + + this.setTrace() + } + + setMetaStep(metaStep) { + this.metaStep = metaStep + } + + run() { + throw new Error('Not implemented') + } + + /** + * @returns {number|undefined} + */ + get timeout() { + return getCurrentTimeout(this.timeouts) + } + + /** + * @param {number} timeout - timeout in milliseconds or 0 if no timeout + * @param {number} order - order defines the priority of timeout, timeouts set with lower order override those set with higher order. + * When order below 0 value of timeout only override if new value is lower + */ + setTimeout(timeout, order) { + this.timeouts.set(order, timeout) + } + + /** @function */ + setTrace() { + Error.captureStackTrace(this) + } + + /** @param {Array<*>} args */ + setArguments(args) { + this.args = args + } + + setActor(actor) { + this.actor = actor || '' + } + + /** @param {string} status */ + setStatus(status) { + this.status = status + if (this.metaStep) { + this.metaStep.setStatus(status) + } + } + + /** @return {string} */ + humanize() { + return humanizeString(this.name) + } + + /** @return {string} */ + humanizeArgs() { + return this.args + .map(arg => { + if (!arg) { + return '' + } + if (typeof arg === 'string') { + return `"${arg}"` + } + if (Array.isArray(arg)) { + try { + const res = JSON.stringify(arg) + return res + } catch (err) { + return `[${arg.toString()}]` + } + } else if (typeof arg === 'function') { + return arg.toString() + } else if (typeof arg === 'undefined') { + return `${arg}` + } else if (arg instanceof Secret) { + return arg.getMasked() + } else if (arg.toString && arg.toString() !== '[object Object]') { + return arg.toString() + } else if (typeof arg === 'object') { + const returnedArg = {} + for (const [key, value] of Object.entries(arg)) { + returnedArg[key] = value + if (value instanceof Secret) returnedArg[key] = value.getMasked() + } + return JSON.stringify(returnedArg) + } + return arg + }) + .join(', ') + } + + /** @return {string} */ + line() { + const lines = this.stack.split('\n') + if (lines[STACK_LINE]) { + return lines[STACK_LINE].trim() + .replace(global.codecept_dir || '', '.') + .trim() + } + return '' + } + + /** @return {string} */ + toString() { + return ucfirst(`${this.prefix}${this.actor} ${this.humanize()} ${this.humanizeArgs()}${this.suffix}`).trim() + } + + /** @return {string} */ + toCliStyled() { + return `${this.prefix}${this.actor} ${color.italic(this.humanize())} ${color.yellow(this.humanizeArgs())}${this.suffix}` + } + + /** @return {string} */ + toCode() { + return `${this.prefix}${this.actor}.${this.name}(${this.humanizeArgs()})${this.suffix}` + } + + isMetaStep() { + return this.constructor.name === 'MetaStep' + } + + /** @return {boolean} */ + hasBDDAncestor() { + let hasBDD = false + let processingStep + processingStep = this + + while (processingStep.metaStep) { + if (processingStep.metaStep.actor.match(/^(Given|When|Then|And)/)) { + hasBDD = true + break + } else { + processingStep = processingStep.metaStep + } + } + return hasBDD + } +} + +module.exports = Step diff --git a/lib/step/config.js b/lib/step/config.js new file mode 100644 index 000000000..739f342b1 --- /dev/null +++ b/lib/step/config.js @@ -0,0 +1,50 @@ +/** + * StepConfig is a configuration object for a step. + * It is used to create a new step that is a combination of other steps. + */ +class StepConfig { + constructor(opts = {}) { + /** @member {{ opts: Record, timeout: number|undefined, retry: number|undefined }} */ + this.config = { + opts, + timeout: undefined, + retry: undefined, + } + } + + /** + * Set the options for the step. + * @param {object} opts - The options for the step. + * @returns {StepConfig} - The step configuration object. + */ + opts(opts) { + this.config.opts = opts + return this + } + + /** + * Set the timeout for the step. + * @param {number} timeout - The timeout for the step. + * @returns {StepConfig} - The step configuration object. + */ + timeout(timeout) { + this.config.timeout = timeout + return this + } + + /** + * Set the retry for the step. + * @param {number} retry - The retry for the step. + * @returns {StepConfig} - The step configuration object. + */ + retry(retry) { + this.config.retry = retry + return this + } + + getConfig() { + return this.config + } +} + +module.exports = StepConfig diff --git a/lib/step/helper.js b/lib/step/helper.js new file mode 100644 index 000000000..b52470e3c --- /dev/null +++ b/lib/step/helper.js @@ -0,0 +1,47 @@ +const Step = require('./base') +const store = require('../store') + +class HelperStep extends Step { + constructor(helper, name) { + super(name) + /** @member {CodeceptJS.Helper} helper corresponding helper */ + this.helper = helper + /** @member {string} helperMethod name of method to be executed */ + this.helperMethod = name + } + + /** + * @param {...any} args + * @return {*} + */ + run() { + this.args = Array.prototype.slice.call(arguments) + + if (store.dryRun) { + this.setStatus('success') + return Promise.resolve(new Proxy({}, dryRunResolver())) + } + let result + try { + if (this.helperMethod !== 'say') { + result = this.helper[this.helperMethod].apply(this.helper, this.args) + } + this.setStatus('success') + } catch (err) { + this.setStatus('failed') + throw err + } + return result + } +} + +module.exports = HelperStep + +function dryRunResolver() { + return { + get(target, prop) { + if (prop === 'toString') return () => '' + return new Proxy({}, dryRunResolver()) + }, + } +} diff --git a/lib/step/meta.js b/lib/step/meta.js new file mode 100644 index 000000000..bc2b0a39e --- /dev/null +++ b/lib/step/meta.js @@ -0,0 +1,91 @@ +const Step = require('./base') +const event = require('../event') +const { humanizeString } = require('../utils') + +class MetaStep extends Step { + constructor(actor, method) { + if (!method) method = '' + super(method) + this.actor = actor + } + + /** @return {boolean} */ + isBDD() { + if (this.actor && this.actor.match && this.actor.match(/^(Given|When|Then|And)/)) { + return true + } + return false + } + + toCliStyled() { + return this.toString() + } + + toString() { + const actorText = this.actor + + if (this.isBDD()) { + return `${this.prefix}${actorText} ${this.name} "${this.humanizeArgs()}${this.suffix}"` + } + + if (actorText === 'I') { + return `${this.prefix}${actorText} ${this.humanize()} ${this.humanizeArgs()}${this.suffix}` + } + + return `On ${this.prefix}${actorText}: ${this.humanize()} ${this.humanizeArgs()}${this.suffix}` + } + + humanize() { + return humanizeString(this.name) + } + + setTrace() {} + + setContext(context) { + this.context = context + } + + /** @return {*} */ + run(fn) { + this.status = 'queued' + this.setArguments(Array.from(arguments).slice(1)) + let result + + const registerStep = step => { + this.setMetaStep(null) + step.setMetaStep(this) + } + event.dispatcher.prependListener(event.step.before, registerStep) + // Handle async and sync methods. + if (fn.constructor.name === 'AsyncFunction') { + result = fn + .apply(this.context, this.args) + .then(result => { + return result + }) + .catch(error => { + this.setStatus('failed') + throw error + }) + .finally(() => { + this.endTime = Date.now() + event.dispatcher.removeListener(event.step.before, registerStep) + }) + } else { + try { + this.startTime = Date.now() + result = fn.apply(this.context, this.args) + } catch (error) { + this.setStatus('failed') + throw error + } finally { + this.endTime = Date.now() + event.dispatcher.removeListener(event.step.before, registerStep) + } + } + + return result + } +} + +module.exports = MetaStep diff --git a/lib/step/record.js b/lib/step/record.js new file mode 100644 index 000000000..40922b401 --- /dev/null +++ b/lib/step/record.js @@ -0,0 +1,74 @@ +const event = require('../event') +const recorder = require('../recorder') +const StepConfig = require('./config') +const { debug } = require('../output') +const store = require('../store') +const { TIMEOUT_ORDER } = require('./timeout') +const retryStep = require('./retry') +function recordStep(step, args) { + step.status = 'queued' + + // apply step configuration + const lastArg = args[args.length - 1] + if (lastArg instanceof StepConfig) { + const stepConfig = args.pop() + const { opts, timeout, retry } = stepConfig.getConfig() + + if (opts) { + debug(`Step ${step.name}: options applied ${JSON.stringify(opts)}`) + store.stepOptions = opts + step.opts = opts + } + if (timeout) { + debug(`Step ${step.name} timeout ${timeout}s`) + step.setTimeout(timeout * 1000, TIMEOUT_ORDER.codeLimitTime) + } + if (retry) retryStep(retry) + } + + step.setArguments(args) + // run async before step hooks + event.emit(event.step.before, step) + + const task = `${step.name}: ${step.humanizeArgs()}` + let val + + // run step inside promise + recorder.add( + task, + () => { + if (!step.startTime) { + // step can be retries + event.emit(event.step.started, step) + step.startTime = Date.now() + } + return (val = step.run(...args)) + }, + false, + undefined, + step.timeout, + ) + + event.emit(event.step.after, step) + + recorder.add('step passed', () => { + step.endTime = Date.now() + event.emit(event.step.passed, step, val) + event.emit(event.step.finished, step) + }) + + recorder.catchWithoutStop(err => { + step.status = 'failed' + step.endTime = Date.now() + event.emit(event.step.failed, step) + event.emit(event.step.finished, step) + throw err + }) + + recorder.add('return result', () => val) + // run async after step hooks + + return recorder.promise() +} + +module.exports = recordStep diff --git a/lib/step/retry.js b/lib/step/retry.js new file mode 100644 index 000000000..d6a47317c --- /dev/null +++ b/lib/step/retry.js @@ -0,0 +1,11 @@ +const recorder = require('../recorder') +const event = require('../event') + +function retryStep(opts) { + if (opts === undefined) opts = 1 + recorder.retry(opts) + // remove retry once the step passed + recorder.add(() => event.dispatcher.once(event.step.finished, () => recorder.retries.pop())) +} + +module.exports = retryStep diff --git a/lib/step/timeout.js b/lib/step/timeout.js new file mode 100644 index 000000000..876644d41 --- /dev/null +++ b/lib/step/timeout.js @@ -0,0 +1,42 @@ +const TIMEOUT_ORDER = { + /** + * timeouts set with order below zero only override timeouts of higher order if their value is smaller + */ + testOrSuite: -5, + /** + * 0-9 - designated for override of timeouts set from code, 5 is used by stepTimeout plugin when stepTimeout.config.overrideStepLimits=true + */ + stepTimeoutHard: 5, + /** + * 10-19 - designated for timeouts set from code, 15 is order of I.setTimeout(t) operation + */ + codeLimitTime: 15, + /** + * 20-29 - designated for timeout settings which could be overriden in tests code, 25 is used by stepTimeout plugin when stepTimeout.config.overrideStepLimits=false + */ + stepTimeoutSoft: 25, +} + +function getCurrentTimeout(timeouts) { + let totalTimeout + // iterate over all timeouts starting from highest values of order + new Map([...timeouts.entries()].sort().reverse()).forEach((timeout, order) => { + if ( + timeout !== undefined && + // when orders >= 0 - timeout value overrides those set with higher order elements + (order >= 0 || + // when `order < 0 && totalTimeout === undefined` - timeout is used when nothing is set by elements with higher order + totalTimeout === undefined || + // when `order < 0` - timeout overrides higher values of timeout or 'no timeout' (totalTimeout === 0) set by elements with higher order + (timeout > 0 && (timeout < totalTimeout || totalTimeout === 0))) + ) { + totalTimeout = timeout + } + }) + return totalTimeout +} + +module.exports = { + TIMEOUT_ORDER, + getCurrentTimeout, +} diff --git a/lib/steps.js b/lib/steps.js new file mode 100644 index 000000000..fc126629d --- /dev/null +++ b/lib/steps.js @@ -0,0 +1,23 @@ +const StepConfig = require('./step/config') + +function stepOpts(opts = {}) { + return new StepConfig(opts) +} + +function stepTimeout(timeout) { + return new StepConfig().timeout(timeout) +} + +function stepRetry(retry) { + return new StepConfig().retry(retry) +} + +// Section function to be added here + +const step = { + opts: stepOpts, + timeout: stepTimeout, + retry: stepRetry, +} + +module.exports = step diff --git a/lib/store.js b/lib/store.js index 9a29577d6..352b2d27c 100644 --- a/lib/store.js +++ b/lib/store.js @@ -13,6 +13,8 @@ const store = { onPause: false, /** @type {CodeceptJS.Test | null} */ currentTest: null, + /** @type {any} */ + currentStep: null, } module.exports = store diff --git a/lib/utils.js b/lib/utils.js index 3c28b2696..2aac45685 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -531,3 +531,14 @@ module.exports.searchWithFusejs = function (source, searchString, opts) { return fuse.search(searchString) } + +module.exports.humanizeString = function (string) { + // split strings by words, then make them all lowercase + const _result = string + .replace(/([a-z](?=[A-Z]))/g, '$1 ') + .split(' ') + .map(word => word.toLowerCase()) + + _result[0] = _result[0] === 'i' ? this.ucfirst(_result[0]) : _result[0] + return _result.join(' ').trim() +} diff --git a/lib/within.js b/lib/within.js index 8893b4c74..119b45406 100644 --- a/lib/within.js +++ b/lib/within.js @@ -3,7 +3,7 @@ const store = require('./store') const recorder = require('./recorder') const container = require('./container') const event = require('./event') -const Step = require('./step') +const MetaStep = require('./step/meta') const { isAsyncFunction } = require('./utils') /** @@ -76,7 +76,7 @@ function within(context, fn) { module.exports = within -class WithinStep extends Step.MetaStep { +class WithinStep extends MetaStep { constructor(locator, fn) { super('Within') this.args = [locator] diff --git a/package.json b/package.json index b661b3f32..49562a469 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,9 @@ "main": "lib/index.js", "exports": { ".": "./lib/index.js", - "./els": "./lib/els.js" + "./els": "./lib/els.js", + "./effects": "./lib/effects.js", + "./steps": "./lib/steps.js" }, "types": "typings/index.d.ts", "bin": { diff --git a/runok.js b/runok.js index 2549582ac..8bfa171db 100755 --- a/runok.js +++ b/runok.js @@ -546,6 +546,73 @@ ${changelog}` process.exit(1) } }, + + async runnerCreateTests(featureName) { + // create runner tests for feature + const fs = require('fs').promises + const path = require('path') + + // Create directories + const configDir = path.join('test/data/sandbox/configs', featureName) + await fs.mkdir(configDir, { recursive: true }) + + // Create codecept config file + const configContent = `exports.config = { + tests: './*_test.js', + output: './output', + helpers: { + FileSystem: {}, + }, + include: {}, + bootstrap: false, + mocha: {}, + name: '${featureName} tests' + } + ` + await fs.writeFile(path.join(configDir, `codecept.conf.js`), configContent) + + // Create feature test file + const testContent = `Feature('${featureName}'); + +Scenario('test ${featureName}', ({ I }) => { + // Add test steps here +}); +` + await fs.writeFile(path.join(configDir, `${featureName}_test.js`), testContent) + + // Create runner test file + const runnerTestContent = `const { expect } = require('expect') +const exec = require('child_process').exec +const { codecept_dir, codecept_run } = require('./consts') +const debug = require('debug')('codeceptjs:tests') + +const config_run_config = (config, grep, verbose = false) => + \`\${codecept_run} \${verbose ? '--verbose' : ''} --config \${codecept_dir}/configs/${featureName}/\${config} \${grep ? \`--grep "\${grep}"\` : ''}\` + +describe('CodeceptJS ${featureName}', function () { + this.timeout(10000) + + it('should run ${featureName} test', done => { + exec(config_run_config('codecept.conf.js'), (err, stdout) => { + debug(stdout) + expect(stdout).toContain('OK') + expect(err).toBeFalsy() + done() + }) + }) +}) +` + await fs.writeFile(path.join('test/runner', `${featureName}_test.js`), runnerTestContent) + + console.log(`Created test files for feature: ${featureName}`) + + console.log('Run codecept tests with:') + console.log(`./bin/codecept.js run --config ${configDir}/codecept.${featureName}.conf.js`) + + console.log('') + console.log('Run tests with:') + console.log(`npx mocha test/runner --grep ${featureName}`) + }, } async function processChangelog() { diff --git a/test/data/sandbox/configs/step-enhancements/codecept.conf.js b/test/data/sandbox/configs/step-enhancements/codecept.conf.js new file mode 100644 index 000000000..8ec600fbf --- /dev/null +++ b/test/data/sandbox/configs/step-enhancements/codecept.conf.js @@ -0,0 +1,14 @@ +exports.config = { + tests: './*_test.js', + output: './output', + helpers: { + FileSystem: {}, + CustomHelper: { + require: './custom_helper.js', + }, + }, + include: {}, + bootstrap: false, + mocha: {}, + name: 'step-enhancements tests', +} diff --git a/test/data/sandbox/configs/step-enhancements/custom_helper.js b/test/data/sandbox/configs/step-enhancements/custom_helper.js new file mode 100644 index 000000000..7ce5002fb --- /dev/null +++ b/test/data/sandbox/configs/step-enhancements/custom_helper.js @@ -0,0 +1,24 @@ +const { store } = require('codeceptjs') + +let retryCount = 0 + +class MyHelper { + retryFewTimesAndPass(num) { + if (retryCount < num) { + retryCount++ + throw new Error('Failed on try ' + retryCount) + } + } + + wait(timeout) { + return new Promise(resolve => setTimeout(resolve, timeout)) + } + + printOption() { + if (store.currentStep?.opts) { + console.log('Option:', store.currentStep?.opts?.text) + } + } +} + +module.exports = MyHelper diff --git a/test/data/sandbox/configs/step-enhancements/step-enhancements_test.js b/test/data/sandbox/configs/step-enhancements/step-enhancements_test.js new file mode 100644 index 000000000..7e949fe75 --- /dev/null +++ b/test/data/sandbox/configs/step-enhancements/step-enhancements_test.js @@ -0,0 +1,14 @@ +const step = require('codeceptjs/steps') +Feature('step-enhancements') + +Scenario('test step opts', ({ I }) => { + I.printOption(step.opts({ text: 'Hello' })) +}) + +Scenario('test step timeouts', ({ I }) => { + I.wait(1000, step.timeout(0.1)) +}) + +Scenario('test step retry', ({ I }) => { + I.retryFewTimesAndPass(3, step.retry(4)) +}) diff --git a/test/data/sandbox/configs/timeouts/suite_test.js b/test/data/sandbox/configs/timeouts/suite_test.js index 64a2e015e..f53f2bfb7 100644 --- a/test/data/sandbox/configs/timeouts/suite_test.js +++ b/test/data/sandbox/configs/timeouts/suite_test.js @@ -1,14 +1,21 @@ -Feature('no timeout'); +const step = require('codeceptjs/steps') + +Feature('no timeout') Scenario('no timeout test #first', ({ I }) => { - I.waitForSleep(1000); -}); + I.waitForSleep(1000) +}) Scenario('timeout test in 0.5 #second', { timeout: 0.5 }, ({ I }) => { - I.waitForSleep(1000); -}); + I.waitForSleep(1000) +}) Scenario('timeout step in 0.5', ({ I }) => { - I.limitTime(0.2).waitForSleep(100); - I.limitTime(0.1).waitForSleep(3000); -}); + I.limitTime(0.2).waitForSleep(100) + I.limitTime(0.2).waitForSleep(3000) +}) + +Scenario('timeout step in 0.5 new syntax', ({ I }) => { + I.waitForSleep(100, step.timeout(0.2)) + I.waitForSleep(3000, step.timeout(0.2)) +}) diff --git a/test/helper/webapi.js b/test/helper/webapi.js index 87592c343..489dcad1d 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -1,5 +1,5 @@ const chai = require('chai') - +const store = require('../../lib/store') const expect = chai.expect const assert = chai.assert const path = require('path') @@ -101,6 +101,17 @@ module.exports.tests = function () { await I.dontSee('Info') }) + it('should check text on site with ignoreCase option', async () => { + if (isHelper('TestCafe')) return // It won't be implemented + await I.amOnPage('/') + await I.see('Welcome') + store.currentStep = { opts: { ignoreCase: true } } + await I.see('welcome to test app!') + await I.see('test link', 'a') + store.currentStep = {} + await I.dontSee('welcome') + }) + it('should check text inside element', async () => { await I.amOnPage('/') await I.see('Welcome to test app!', 'h1') diff --git a/test/runner/step-enhancements_test.js b/test/runner/step-enhancements_test.js new file mode 100644 index 000000000..b988562e4 --- /dev/null +++ b/test/runner/step-enhancements_test.js @@ -0,0 +1,42 @@ +const { expect } = require('expect') +const exec = require('child_process').exec +const { codecept_dir, codecept_run } = require('./consts') +const debug = require('debug')('codeceptjs:tests') + +const config_run_config = (config, grep, verbose = false) => `${codecept_run} ${verbose ? '--verbose' : ''} --config ${codecept_dir}/configs/step-enhancements/${config} ${grep ? `--grep "${grep}"` : ''}` + +describe('CodeceptJS step-enhancements', function () { + this.timeout(10000) + + it('should apply step options', done => { + exec(config_run_config('codecept.conf.js', 'opts', true), (err, stdout) => { + debug(stdout) + expect(stdout).toContain('Option: Hello') + expect(stdout).toContain('options applied {"text":"Hello"}') + expect(stdout).toContain('print option') + expect(stdout).not.toContain('print option {"text":"Hello"}') + expect(stdout).toContain('OK') + expect(err).toBeFalsy() + done() + }) + }) + + it('should apply step timeouts', done => { + exec(config_run_config('codecept.conf.js', 'timeouts', true), (err, stdout) => { + debug(stdout) + expect(err).toBeTruthy() + expect(stdout).not.toContain('OK') + expect(stdout).toContain('was interrupted on step timeout 100ms') + done() + }) + }) + + it('should apply step retry', done => { + exec(config_run_config('codecept.conf.js', 'retry', true), (err, stdout) => { + debug(stdout) + expect(stdout).toContain('OK') + expect(err).toBeFalsy() + done() + }) + }) +}) diff --git a/test/runner/timeout_test.js b/test/runner/timeout_test.js index 5e640f63c..a3c1ffefe 100644 --- a/test/runner/timeout_test.js +++ b/test/runner/timeout_test.js @@ -40,6 +40,7 @@ describe('CodeceptJS Timeouts', function () { }) it('should use global timeouts if timeout is set', done => { + this.retries(3) exec(config_run_config('codecept.timeout.conf.js', 'no timeout test'), (err, stdout) => { debug_this_test && console.log(stdout) expect(stdout).toContain('Timeout 0.1') @@ -51,7 +52,7 @@ describe('CodeceptJS Timeouts', function () { it('should prefer step timeout', done => { exec(config_run_config('codecept.conf.js', 'timeout step', true), (err, stdout) => { debug_this_test && console.log(stdout) - expect(stdout).toContain('was interrupted on step timeout 100ms') + expect(stdout).toContain('was interrupted on step timeout 200ms') expect(err).toBeTruthy() done() }) diff --git a/typings/index.d.ts b/typings/index.d.ts index fb6a68aac..2bc730c77 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -451,9 +451,6 @@ declare namespace CodeceptJS { } // Extending JSDoc generated typings - interface Step { - isMetaStep(): this is MetaStep - } // Types who are not be defined by JSDoc type actor = void }>(customSteps?: T & ThisType>) => WithTranslation diff --git a/typings/jsdoc.conf.js b/typings/jsdoc.conf.js index 3ff9b4c04..ca1d115bf 100644 --- a/typings/jsdoc.conf.js +++ b/typings/jsdoc.conf.js @@ -16,7 +16,10 @@ module.exports = { './lib/recorder.js', './lib/secret.js', './lib/session.js', - './lib/step.js', + './lib/step/config.js', + './lib/step/base.js', + './lib/step/helper.js', + './lib/step/meta.js', './lib/store.js', './lib/mocha/ui.js', './lib/mocha/featureConfig.js',