Repository: codeceptjs/CodeceptJS (js)

Issue:
Display custom steps in a human-friendly way
#### What are you trying to achieve?

I would like custom steps to be displayed in the same, human-friendly way as predefined steps.
e.g I should see:
```
successful login with email and password
    I try to log in with credentials 
       - I fill field "email-input", "email"
       - I fill field "password-input", "password"
```
where the first line is the scenario text, the second line is my custom step, specified by a function called `tryToLogInWithCredentials` on the actor, and the next two lines are smaller, low-level predefined steps specified by the `fillField` function on an actor.

#### What do you get instead?

```
successful login with email and password
    I: tryToLogInWithCredentials 
    I fill field "email-input", "email"
    I fill field "password-input", "password"
```


### Details

* CodeceptJS version: 2.0.6
* NodeJS Version: 8.11.0
* Operating System: 
* Protractor || WebDriverIO || Nightmare version (if related)
* Configuration file:

```js
exports.config = {
  tests: './tests/*_test.js',
  output: './output',
  helpers: {
    Puppeteer: {
      url: 'http://localhost'
    }
  },
  include: {
    I: './steps_file.js'
  },
  bootstrap: null,
  mocha: {},
  name: 'app'
}
```

Had a quick look at the source code, I think the `humanize` function from `step.js` could be used, I am happy to help with implementation if you decide this would be useful.

Pull request description:
fix: display custom steps friendly
## Motivation/Description of the PR
- Description of this PR, which problem it solves
- Resolves #1534 

```
login --
  Incorrect username or password.
    I get user per page 2
      I send get request "/api/users?page=2"
    I am on page "/login"
    I fill field "Username or email address", "michael.lawson@reqres.in"
    I fill field "Password", *****
    I click "Sign in"
    I see "Incorrect username or password.", ".flash-error"
  ✔ OK in 1780ms


  OK  | 1 passed   // 3s
```

## Type of change
- [ ] :bug: Bug fix

## Checklist:
- [ ] Tests have been added
- [ ] Documentation has been added (Run `npm run docs`)
- [ ] Lint checking (Run `npm run lint`)
- [ ] Local tests are passed (Run `npm test`)


Patch:
diff --git a/docs/helpers/GraphQL.md b/docs/helpers/GraphQL.md
index df69292d..82d7bbfa 100644
--- a/docs/helpers/GraphQL.md
+++ b/docs/helpers/GraphQL.md
@@ -68,6 +68,27 @@ Prepares request for axios call
 
 Returns **[object][2]** graphQLRequest
 
+### amBearerAuthenticated
+
+Adds a header for Bearer authentication
+
+```js
+// we use secret function to hide token from logs
+I.amBearerAuthenticated(secret('heregoestoken'))
+```
+
+#### Parameters
+
+-   `accessToken` **([string][3] | CodeceptJS.Secret)** Bearer access token
+
+### haveRequestHeaders
+
+Sets request headers for all requests of this test
+
+#### Parameters
+
+-   `headers` **[object][2]** headers list
+
 ### sendMutation
 
 Send query to GraphQL endpoint over http
diff --git a/docs/helpers/Playwright.md b/docs/helpers/Playwright.md
index b85b3ebe..17511c27 100644
--- a/docs/helpers/Playwright.md
+++ b/docs/helpers/Playwright.md
@@ -299,7 +299,7 @@ const elements = await this.helpers['Playwright']._locate({name: 'password'});
 
 ### _locateCheckable
 
-Find a checkbox by providing human readable text:
+Find a checkbox by providing human-readable text:
 NOTE: Assumes the checkable element exists
 
 ```js
@@ -313,7 +313,7 @@ this.helpers['Playwright']._locateCheckable('I agree with terms and conditions')
 
 ### _locateClickable
 
-Find a clickable element by providing human readable text:
+Find a clickable element by providing human-readable text:
 
 ```js
 this.helpers['Playwright']._locateClickable('Next page').then // ...
@@ -323,9 +323,22 @@ this.helpers['Playwright']._locateClickable('Next page').then // ...
 
 -   `locator`  
 
+### _locateElement
+
+Get the first element by different locator types, including strict locator
+Should be used in custom helpers:
+
+```js
+const element = await this.helpers['Playwright']._locateElement({name: 'password'});
+```
+
+#### Parameters
+
+-   `locator`  
+
 ### _locateFields
 
-Find field elements by providing human readable text:
+Find field elements by providing human-readable text:
 
 ```js
 this.helpers['Playwright']._locateFields('Your email').then // ...
diff --git a/docs/playwright.md b/docs/playwright.md
index 76d17e1c..ed830714 100644
--- a/docs/playwright.md
+++ b/docs/playwright.md
@@ -272,6 +272,29 @@ exports.config = {
 }
 ```
 
+Sometimes, the Electron app is built with [electron-forge](https://www.electronforge.io/), then configuring may a bit different.
+
+- First, you should run your electron-forge command to build and pack your app.
+- Then, you would find `index.js` file inside `.webpack/main/index.js`
+
+`codecept.conf.js` - CodeceptJS configuration file
+```js
+const path = require("path");
+
+exports.config = {
+  helpers: {
+    Playwright: {
+      browser: "electron",
+      electron: {
+        executablePath: require("electron"),
+        args: [path.join(__dirname, ".webpack/main/index.js")],
+      },
+    },
+  },
+  // rest of config
+}
+```
+
 ### Headless Mode
 
 With Electron, headless mode must be set when creating the window. Therefore, CodeceptJS's `show` configuration parameter will not work. However, you can set it in the `main.js` file as shown below:
@@ -606,4 +629,4 @@ Playwright can be added to GitHub Actions using [official action](https://github
 - uses: microsoft/playwright-github-action@v1
 - name: run CodeceptJS tests
   run: npx codeceptjs run
-```
\ No newline at end of file
+```
diff --git a/lib/helper/GraphQL.js b/lib/helper/GraphQL.js
index 5f919684..fb87f16a 100644
--- a/lib/helper/GraphQL.js
+++ b/lib/helper/GraphQL.js
@@ -81,6 +81,8 @@ class GraphQL extends Helper {
       'Content-Type': 'application/json',
     });
 
+    request.headers = { ...this.headers, ...request.headers };
+
     if (this.config.onRequest) {
       await this.config.onRequest(request);
     }
@@ -200,5 +202,28 @@ class GraphQL extends Helper {
   _setRequestTimeout(newTimeout) {
     this.options.timeout = newTimeout;
   }
+
+  /**
+   * Sets request headers for all requests of this test
+   *
+   * @param {object} headers headers list
+   */
+  haveRequestHeaders(headers) {
+    this.headers = { ...this.headers, ...headers };
+  }
+
+  /**
+   * Adds a header for Bearer authentication
+   *
+   * ```js
+   * // we use secret function to hide token from logs
+   * I.amBearerAuthenticated(secret('heregoestoken'))
+   * ```
+   *
+   * @param {string | CodeceptJS.Secret} accessToken  Bearer access token
+   */
+  amBearerAuthenticated(accessToken) {
+    this.haveRequestHeaders({ Authorization: `Bearer ${accessToken}` });
+  }
 }
 module.exports = GraphQL;
diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js
index a6ea7b19..20c4e123 100644
--- a/lib/helper/Playwright.js
+++ b/lib/helper/Playwright.js
@@ -23,6 +23,7 @@ const {
   isModifierKey,
   clearString,
   requireWithFallback,
+  normalizeSpacesInString,
 } = require('../utils');
 const {
   isColorProperty,
@@ -825,9 +826,9 @@ class Playwright extends Helper {
       return;
     }
 
-    const els = await this._locate(locator);
-    assertElementExists(els, locator);
-    this.context = els[0];
+    const el = await this._locateElement(locator);
+    assertElementExists(el, locator);
+    this.context = el;
     this.contextLocator = locator;
 
     this.withinLocator = new Locator(locator);
@@ -930,11 +931,11 @@ class Playwright extends Helper {
    *
    */
   async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
-    const els = await this._locate(locator);
-    assertElementExists(els, locator);
+    const el = await this._locateElement(locator);
+    assertElementExists(el, locator);
 
     // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
-    const { x, y } = await clickablePoint(els[0]);
+    const { x, y } = await clickablePoint(el);
     await this.page.mouse.move(x + offsetX, y + offsetY);
     return this._waitForAction();
   }
@@ -944,9 +945,8 @@ class Playwright extends Helper {
    *
    */
   async focus(locator, options = {}) {
-    const els = await this._locate(locator);
-    assertElementExists(els, locator, 'Element to focus');
-    const el = els[0];
+    const el = await this._locateElement(locator);
+    assertElementExists(el, locator, 'Element to focus');
 
     await el.focus(options);
     return this._waitForAction();
@@ -957,12 +957,10 @@ class Playwright extends Helper {
    *
    */
   async blur(locator, options = {}) {
-    const els = await this._locate(locator);
-    assertElementExists(els, locator, 'Element to blur');
-    // TODO: locator change required after #3677 implementation
-    const elXpath = await getXPathForElement(els[0]);
+    const el = await this._locateElement(locator);
+    assertElementExists(el, locator, 'Element to blur');
 
-    await this.page.locator(elXpath).blur(options);
+    await el.blur(options);
     return this._waitForAction();
   }
 
@@ -1062,10 +1060,10 @@ class Playwright extends Helper {
     }
 
     if (locator) {
-      const els = await this._locate(locator);
-      assertElementExists(els, locator, 'Element');
-      await els[0].scrollIntoViewIfNeeded();
-      const elementCoordinates = await clickablePoint(els[0]);
+      const el = await this._locateElement(locator);
+      assertElementExists(el, locator, 'Element');
+      await el.scrollIntoViewIfNeeded();
+      const elementCoordinates = await clickablePoint(el);
       await this.executeScript((offsetX, offsetY) => window.scrollBy(offsetX, offsetY), { offsetX: elementCoordinates.x + offsetX, offsetY: elementCoordinates.y + offsetY });
     } else {
       await this.executeScript(({ offsetX, offsetY }) => window.scrollTo(offsetX, offsetY), { offsetX, offsetY });
@@ -1133,7 +1131,20 @@ class Playwright extends Helper {
   }
 
   /**
-   * Find a checkbox by providing human readable text:
+   * Get the first element by different locator types, including strict locator
+   * Should be used in custom helpers:
+   *
+   * ```js
+   * const element = await this.helpers['Playwright']._locateElement({name: 'password'});
+   * ```
+   */
+  async _locateElement(locator) {
+    const context = await this.context || await this._getContext();
+    return findElement(context, locator);
+  }
+
+  /**
+   * Find a checkbox by providing human-readable text:
    * NOTE: Assumes the checkable element exists
    *
    * ```js
@@ -1148,7 +1159,7 @@ class Playwright extends Helper {
   }
 
   /**
-   * Find a clickable element by providing human readable text:
+   * Find a clickable element by providing human-readable text:
    *
    * ```js
    * this.helpers['Playwright']._locateClickable('Next page').then // ...
@@ -1160,7 +1171,7 @@ class Playwright extends Helper {
   }
 
   /**
-   * Find field elements by providing human readable text:
+   * Find field elements by providing human-readable text:
    *
    * ```js
    * this.helpers['Playwright']._locateFields('Your email').then // ...
@@ -1536,13 +1547,8 @@ class Playwright extends Helper {
     const els = await findFields.call(this, field);
     assertElementExists(els, field, 'Field');
     const el = els[0];
-    const tag = await el.getProperty('tagName').then(el => el.jsonValue());
-    const editable = await el.getProperty('contenteditable').then(el => el.jsonValue());
-    if (tag === 'INPUT' || tag === 'TEXTAREA') {
-      await this._evaluateHandeInContext(el => el.value = '', el);
-    } else if (editable) {
-      await this._evaluateHandeInContext(el => el.innerHTML = '', el);
-    }
+
+    await el.clear();
 
     highlightActiveElement.call(this, el, this.page);
 
@@ -1569,21 +1575,16 @@ class Playwright extends Helper {
    * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
    */
   async clearField(locator, options = {}) {
-    let result;
-    const isNewClearMethodPresent = false; // not works, disabled for now. Prev: typeof this.page.locator().clear === 'function';
+    const els = await findFields.call(this, locator);
+    assertElementExists(els, locator, 'Field to clear');
 
-    if (isNewClearMethodPresent) {
-      const els = await findFields.call(this, locator);
-      assertElementExists(els, locator, 'Field to clear');
-      // TODO: locator change required after #3677 implementation
-      const elXpath = await getXPathForElement(els[0]);
+    const el = els[0];
 
-      await this.page.locator(elXpath).clear(options);
-      result = await this._waitForAction();
-    } else {
-      result = await this.fillField(locator, '');
-    }
-    return result;
+    highlightActiveElement.call(this, el, this.page);
+
+    await el.clear();
+
+    return this._waitForAction();
   }
 
   /**
@@ -1637,29 +1638,11 @@ class Playwright extends Helper {
     const els = await findFields.call(this, select);
     assertElementExists(els, select, 'Selectable field');
     const el = els[0];
-    if (await el.getProperty('tagName').then(t => t.jsonValue()) !== 'SELECT') {
-      throw new Error('Element is not <select>');
-    }
+
     highlightActiveElement.call(this, el, this.page);
     if (!Array.isArray(option)) option = [option];
 
-    for (const key in option) {
-      const opt = xpathLocator.literal(option[key]);
-      let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) });
-      if (optEl.length) {
-        this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
-        continue;
-      }
-      optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) });
-      if (optEl.length) {
-        this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
-      }
-    }
-    await this._evaluateHandeInContext((element) => {
-      element.dispatchEvent(new Event('input', { bubbles: true }));
-      element.dispatchEvent(new Event('change', { bubbles: true }));
-    }, el);
-
+    await el.selectOption(option);
     return this._waitForAction();
   }
 
@@ -1910,7 +1893,7 @@ class Playwright extends Helper {
     const els = await this._locate(locator);
     const texts = [];
     for (const el of els) {
-      texts.push(await (await el.getProperty('innerText')).jsonValue());
+      texts.push(await (await el.innerText()));
     }
     this.debug(`Matched ${els.length} elements`);
     return texts;
@@ -1932,7 +1915,7 @@ class Playwright extends Helper {
   async grabValueFromAll(locator) {
     const els = await findFields.call(this, locator);
     this.debug(`Matched ${els.length} elements`);
-    return Promise.all(els.map(el => el.getProperty('value').then(t => t.jsonValue())));
+    return Promise.all(els.map(el => el.inputValue()));
   }
 
   /**
@@ -1951,7 +1934,7 @@ class Playwright extends Helper {
   async grabHTMLFromAll(locator) {
     const els = await this._locate(locator);
     this.debug(`Matched ${els.length} elements`);
-    return Promise.all(els.map(el => el.$eval('xpath=.', element => element.innerHTML, el)));
+    return Promise.all(els.map(el => el.innerHTML()));
   }
 
   /**
@@ -1972,7 +1955,7 @@ class Playwright extends Helper {
   async grabCssPropertyFromAll(locator, cssProperty) {
     const els = await this._locate(locator);
     this.debug(`Matched ${els.length} elements`);
-    const cssValues = await Promise.all(els.map(el => el.$eval('xpath=.', (el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
+    const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
 
     return cssValues;
   }
@@ -1988,21 +1971,20 @@ class Playwright extends Helper {
     const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
     const elemAmount = res.length;
     const commands = [];
-    res.forEach((el) => {
-      Object.keys(cssPropertiesCamelCase).forEach((prop) => {
-        commands.push(el.$eval('xpath=.', (el) => {
-          const style = window.getComputedStyle ? getComputedStyle(el) : el.currentStyle;
-          return JSON.parse(JSON.stringify(style));
-        }, el)
-          .then((props) => {
-            if (isColorProperty(prop)) {
-              return convertColorToRGBA(props[prop]);
-            }
-            return props[prop];
-          }));
+    let props = [];
+
+    for (const element of res) {
+      const cssProperties = await element.evaluate((el) => getComputedStyle(el));
+
+      Object.keys(cssPropertiesCamelCase).forEach(prop => {
+        if (isColorProperty(prop)) {
+          props.push(convertColorToRGBA(cssProperties[prop]));
+        } else {
+          props.push(cssProperties[prop]);
+        }
       });
-    });
-    let props = await Promise.all(commands);
+    }
+
     const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
     if (!Array.isArray(props)) props = [props];
     let chunked = chunkArray(props, values.length);
@@ -2028,7 +2010,7 @@ class Playwright extends Helper {
     res.forEach((el) => {
       Object.keys(attributes).forEach((prop) => {
         commands.push(el
-          .$eval('xpath=.', (el, attr) => el[attr] || el.getAttribute(attr), prop));
+          .evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop));
       });
     });
     let attrs = await Promise.all(commands);
@@ -2049,11 +2031,11 @@ class Playwright extends Helper {
    *
    */
   async dragSlider(locator, offsetX = 0) {
-    const src = await this._locate(locator);
+    const src = await this._locateElement(locator);
     assertElementExists(src, locator, 'Slider Element');
 
     // Note: Using clickablePoint private api because the .BoundingBox does not take into account iframe offsets!
-    const sliderSource = await clickablePoint(src[0]);
+    const sliderSource = await clickablePoint(src);
 
     // Drag start point
     await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 });
@@ -2087,8 +2069,7 @@ class Playwright extends Helper {
     const array = [];
 
     for (let index = 0; index < els.length; index++) {
-      const a = await this._evaluateHandeInContext(([el, attr]) => el[attr] || el.getAttribute(attr), [els[index], attr]);
-      array.push(await a.jsonValue());
+      array.push(await els[index].getAttribute(attr));
     }
 
     return array;
@@ -2101,10 +2082,9 @@ class Playwright extends Helper {
   async saveElementScreenshot(locator, fileName) {
     const outputFile = screenshotOutputFolder(fileName);
 
-    const res = await this._locate(locator);
+    const res = await this._locateElement(locator);
     assertElementExists(res, locator);
-    if (res.length > 1) this.debug(`[Elements] Using first element out of ${res.length}`);
-    const elem = res[0];
+    const elem = res;
     this.debug(`Screenshot of ${(new Locator(locator))} element has been saved to ${outputFile}`);
     return elem.screenshot({ path: outputFile, type: 'png' });
   }
@@ -2530,16 +2510,26 @@ class Playwright extends Helper {
       }
       return;
     }
+    let contentFrame;
+
     if (!locator) {
-      this.context = this.page;
+      this.context = await this.page.frames()[0];
       this.contextLocator = null;
       return;
     }
 
     // iframe by selector
     const els = await this._locate(locator);
-    assertElementExists(els, locator);
-    const contentFrame = await els[0].contentFrame();
+    // assertElementExists(els, locator);
+
+    // get content of the first iframe
+    if ((locator.frame && locator.frame === 'iframe') || locator.toLowerCase() === 'iframe') {
+      contentFrame = await this.page.frames()[1];
+      // get content of the iframe using its name
+    } else if (locator.toLowerCase().includes('name=')) {
+      const frameName = locator.split('=')[1].replace(/"/g, '').replaceAll(/]/g, '');
+      contentFrame = await this.page.frame(frameName);
+    }
 
     if (contentFrame) {
       this.context = contentFrame;
@@ -2628,9 +2618,9 @@ class Playwright extends Helper {
    * {{> grabElementBoundingRect }}
    */
   async grabElementBoundingRect(locator, prop) {
-    const els = await this._locate(locator);
-    assertElementExists(els, locator);
-    const rect = await els[0].boundingBox();
+    const el = await this._locateElement(locator);
+    assertElementExists(el, locator);
+    const rect = await el.boundingBox();
     if (prop) return rect[prop];
     return rect;
   }
@@ -3003,42 +2993,19 @@ function buildLocatorString(locator) {
   }
   return locator.simplify();
 }
-// TODO: locator change required after #3677 implementation. Temporary solution before migration. Should be deleted after #3677 implementation
-async function getXPathForElement(elementHandle) {
-  function calculateIndex(node) {
-    let index = 1;
-    let sibling = node.previousElementSibling;
-    while (sibling) {
-      if (sibling.tagName === node.tagName) {
-        index++;
-      }
-      sibling = sibling.previousElementSibling;
-    }
-    return index;
-  }
 
-  function generateXPath(node) {
-    const segments = [];
-    while (node && node.nodeType === Node.ELEMENT_NODE) {
-      if (node.hasAttribute('id')) {
-        segments.unshift(`*[@id="${node.getAttribute('id')}"]`);
-        break;
-      } else {
-        const index = calculateIndex(node);
-        segments.unshift(`${node.localName}[${index}]`);
-        node = node.parentNode;
-      }
-    }
-    return `//${segments.join('/')}`;
-  }
+async function findElements(matcher, locator) {
+  if (locator.react) return findReact(matcher, locator);
+  locator = new Locator(locator, 'css');
 
-  return elementHandle.evaluate(generateXPath);
+  return matcher.locator(buildLocatorString(locator)).all();
 }
 
-async function findElements(matcher, locator) {
+async function findElement(matcher, locator) {
   if (locator.react) return findReact(matcher, locator);
   locator = new Locator(locator, 'css');
-  return matcher.$$(buildLocatorString(locator));
+
+  return matcher.locator(buildLocatorString(locator));
 }
 
 async function getVisibleElements(elements) {
@@ -3068,7 +3035,6 @@ async function proceedClick(locator, context = null, options = {}) {
     assertElementExists(els, locator, 'Clickable element');
   }
 
-  const element = els[0];
   highlightActiveElement.call(this, els[0], this.page);
 
   /*
@@ -3117,28 +3083,28 @@ async function findClickable(matcher, locator) {
 async function proceedSee(assertType, text, context, strict = false) {
   let description;
   let allText;
+
   if (!context) {
     let el = await this.context;
-
     if (el && !el.getProperty) {
       // Fallback to body
-      el = await this.context.$('body');
+      el = await this.page.$('body');
     }
 
-    allText = [await el.getProperty('innerText').then(p => p.jsonValue())];
+    allText = [await el.innerText()];
     description = 'web application';
   } else {
     const locator = new Locator(context, 'css');
     description = `element ${locator.toString()}`;
     const els = await this._locate(locator);
     assertElementExists(els, locator.toString());
-    allText = await Promise.all(els.map(el => el.getProperty('innerText').then(p => p.jsonValue())));
+    allText = await Promise.all(els.map(el => el.innerText()));
   }
 
   if (strict) {
     return allText.map(elText => equals(description)[assertType](text, elText));
   }
-  return stringIncludes(description)[assertType](text, allText.join(' | '));
+  return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')));
 }
 
 async function findCheckable(locator, context) {
@@ -3200,15 +3166,15 @@ async function proceedSeeInField(assertType, field, value) {
   const els = await findFields.call(this, field);
   assertElementExists(els, field, 'Field');
   const el = els[0];
-  const tag = await el.getProperty('tagName').then(el => el.jsonValue());
-  const fieldType = await el.getProperty('type').then(el => el.jsonValue());
+  const tag = await el.evaluate(e => e.tagName);
+  const fieldType = await el.getAttribute('type');
 
   const proceedMultiple = async (elements) => {
     const fields = Array.isArray(elements) ? elements : [elements];
 
     const elementValues = [];
     for (const element of fields) {
-      elementValues.push(await element.getProperty('value').then(el => el.jsonValue()));
+      elementValues.push(await element.inputValue());
     }
 
     if (typeof value === 'boolean') {
@@ -3222,8 +3188,8 @@ async function proceedSeeInField(assertType, field, value) {
   };
 
   if (tag === 'SELECT') {
-    if (await el.getProperty('multiple')) {
-      const selectedOptions = await el.$$('option:checked');
+    if (await el.getAttribute('multiple')) {
+      const selectedOptions = await el.all('option:checked');
       if (!selectedOptions.length) return null;
 
       const options = await filterFieldsByValue(selectedOptions, value, true);
@@ -3247,14 +3213,23 @@ async function proceedSeeInField(assertType, field, value) {
     return proceedMultiple(els[0]);
   }
 
-  const fieldVal = await el.inputValue();
+  let fieldVal;
+
+  try {
+    fieldVal = await el.inputValue();
+  } catch (e) {
+    if (e.message.includes('Error: Node is not an <input>, <textarea> or <select> element')) {
+      fieldVal = await el.innerText();
+    }
+  }
+
   return stringIncludes(`fields by ${field}`)[assertType](value, fieldVal);
 }
 
 async function filterFieldsByValue(elements, value, onlySelected) {
   const matches = [];
   for (const element of elements) {
-    const val = await element.getProperty('value').then(el => el.jsonValue());
+    const val = await element.getAttribute('value');
     let isSelected = true;
     if (onlySelected) {
       isSelected = await elementSelected(element);
@@ -3278,12 +3253,12 @@ async function filterFieldsBySelectionState(elements, state) {
 }
 
 async function elementSelected(element) {
-  const type = await element.getProperty('type').then(el => !!el && el.jsonValue());
+  const type = await element.getAttribute('type');
 
   if (type === 'checkbox' || type === 'radio') {
     return element.isChecked();
   }
-  return element.getProperty('selected').then(el => el.jsonValue());
+  return element.getAttribute('selected');
 }
 
 function isFrameLocator(locator) {
diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js
index a3d684f2..5b512768 100644
--- a/lib/helper/Puppeteer.js
+++ b/lib/helper/Puppeteer.js
@@ -23,7 +23,7 @@ const {
   screenshotOutputFolder,
   getNormalizedKeyAttributeValue,
   isModifierKey,
-  requireWithFallback,
+  requireWithFallback, normalizeSpacesInString,
 } = require('../utils');
 const {
   isColorProperty,
@@ -2423,7 +2423,7 @@ async function proceedSee(assertType, text, context, strict = false) {
   if (strict) {
     return allText.map(elText => equals(description)[assertType](text, elText));
   }
-  return stringIncludes(description)[assertType](text, allText.join(' | '));
+  return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | ')));
 }
 
 async function findCheckable(locator, context) {
diff --git a/lib/helper/extras/PlaywrightReact.js b/lib/helper/extras/PlaywrightReact.js
new file mode 100644
index 00000000..3246122b
--- /dev/null
+++ b/lib/helper/extras/PlaywrightReact.js
@@ -0,0 +1,9 @@
+module.exports = async function findReact(matcher, locator) {
+  let _locator = `_react=${locator.react}`;
+
+  if (locator.props && locator.props.name) {
+    _locator += `[name = "${locator.props.name}"]`;
+  }
+
+  return matcher.locator(_locator).all();
+};
diff --git a/lib/step.js b/lib/step.js
index 2e642f23..f57dc2dc 100644
--- a/lib/step.js
+++ b/lib/step.js
@@ -138,13 +138,7 @@ class Step {
 
   /** @return {string} */
   humanize() {
-    return this.name
-    // insert a space before all caps
-      .replace(/([A-Z])/g, ' $1')
-    // _ chars to spaces
-      .replace('_', ' ')
-    // uppercase the first character
-      .replace(/^(.)|\s(.)/g, $1 => $1.toLowerCase());
+    return humanizeString(this.name);
   }
 
   /** @return {string} */
@@ -247,12 +241,21 @@ class MetaStep extends Step {
   }
 
   toString() {
-    const actorText = !this.isBDD() && !this.isWithin() ? `${this.actor}:` : this.actor;
-    return `${this.prefix}${actorText} ${this.humanize()} ${this.humanizeArgs()}${this.suffix}`;
+    const actorText = this.actor;
+
+    if (this.isBDD() || this.isWithin()) {
+      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 this.name;
+    return humanizeString(this.name);
   }
 
   setTrace() {
@@ -316,3 +319,17 @@ function 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));
+}
diff --git a/lib/utils.js b/lib/utils.js
index 308852a8..21383587 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -472,3 +472,7 @@ module.exports.printObjectProperties = (obj) => {
 
   return `{${result}}`;
 };
+
+module.exports.normalizeSpacesInString = (string) => {
+  return string.replace(/\s+/g, ' ');
+};
diff --git a/package.json b/package.json
index fbd0ffa7..283565d4 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
     "lint": "eslint bin/ examples/ lib/ test/ translations/ runok.js",
     "lint-fix": "eslint bin/ examples/ lib/ test/ translations/ runok.js --fix",
     "docs": "./runok.js docs",
-    "test:unit": "mocha test/unit --recursive",
+    "test:unit": "mocha test/unit --recursive --timeout 5000",
     "test:runner": "mocha test/runner --recursive --timeout 5000",
     "test": "npm run test:unit && npm run test:runner",
     "test:appium-quick": "mocha test/helper/Appium_test.js --grep 'quick'",
@@ -60,10 +60,10 @@
   },
   "dependencies": {
     "@codeceptjs/configure": "^0.10.0",
-    "@codeceptjs/helper": "^1.0.2",
+    "@codeceptjs/helper": "^2.0.1",
     "@cucumber/cucumber-expressions": "^16",
     "@cucumber/gherkin": "^26",
-    "@cucumber/messages": "^21.0.1",
+    "@cucumber/messages": "^22.0.0",
     "@xmldom/xmldom": "^0.7.9",
     "acorn": "^8.10.0",
     "arrify": "^2.0.1",
@@ -120,7 +120,7 @@
     "eslint-config-airbnb-base": "^15.0.0",
     "eslint-plugin-import": "^2.25.4",
     "eslint-plugin-mocha": "^6.3.0",
-    "expect": "^26.6.2",
+    "expect": "^29.6.2",
     "express": "^4.17.2",
     "graphql": "^14.6.0",
     "husky": "^8.0.1",


Test patch:
diff --git a/lib/helper/TestCafe.js b/lib/helper/TestCafe.js
index 4125799d..86a44569 100644
--- a/lib/helper/TestCafe.js
+++ b/lib/helper/TestCafe.js
@@ -20,7 +20,7 @@ const { urlEquals } = require('../assert/equal');
 const { empty } = require('../assert/empty');
 const { truth } = require('../assert/truth');
 const {
-  xpathLocator,
+  xpathLocator, normalizeSpacesInString,
 } = require('../utils');
 const Locator = require('../locator');
 
@@ -637,9 +637,9 @@ class TestCafe extends Helper {
   async see(text, context = null) {
     let els;
     if (context) {
-      els = (await findElements.call(this, this.context, context)).withText(text);
+      els = (await findElements.call(this, this.context, context)).withText(normalizeSpacesInString(text));
     } else {
-      els = (await findElements.call(this, this.context, '*')).withText(text);
+      els = (await findElements.call(this, this.context, '*')).withText(normalizeSpacesInString(text));
     }
 
     return this.t
diff --git a/test/acceptance/react_test.js b/test/acceptance/react_test.js
index 8011d515..86913cb9 100644
--- a/test/acceptance/react_test.js
+++ b/test/acceptance/react_test.js
@@ -1,14 +1,14 @@
 Feature('React Selectors');
 
-Scenario('props @WebDriver @Puppeteer @Playwright', ({ I }) => {
+Scenario('props @Puppeteer @Playwright', ({ I }) => {
   I.amOnPage('https://codecept.io/test-react-calculator/');
   I.click('7');
-  I.seeElement({ react: 't', props: { name: '5' } });
-  I.click('button', { react: 't', props: { name: '2' } });
-  I.click('button', { react: 't', props: { name: '+' } });
-  I.click('button', { react: 't', props: { name: '9' } });
-  I.click('button', { react: 't', props: { name: '=' } });
-  I.seeElement({ react: 't', props: { value: '81' } });
+  I.click({ react: 't', props: { name: '=' } });
+  I.seeElement({ react: 't', props: { value: '7' } });
+  I.click({ react: 't', props: { name: '+' } });
+  I.click({ react: 't', props: { name: '3' } });
+  I.click({ react: 't', props: { name: '=' } });
+  I.seeElement({ react: 't', props: { value: '10' } });
 });
 
 Scenario('component name @Puppeteer @Playwright', ({ I }) => {
diff --git a/test/acceptance/within_test.js b/test/acceptance/within_test.js
index 7a04f284..9931c984 100644
--- a/test/acceptance/within_test.js
+++ b/test/acceptance/within_test.js
@@ -1,11 +1,11 @@
 Feature('within', { retries: 3 });
 
-Scenario('within on form @WebDriverIO @Puppeteer @Playwright ', ({ I }) => {
+Scenario('within on form @WebDriverIO @Puppeteer @Playwright', ({ I }) => {
   I.amOnPage('/form/bug1467');
   I.see('TEST TEST');
-  within({ css: '[name=form2]' }, () => {
-    I.checkOption('Yes');
-    I.seeCheckboxIsChecked({ css: 'input[name=first_test_radio]' });
+  within({ css: '[name=form2]' }, async () => {
+    await I.checkOption('Yes');
+    await I.seeCheckboxIsChecked({ css: 'input[name=first_test_radio]' });
   });
   I.seeCheckboxIsChecked({ css: 'form[name=form2] input[name=first_test_radio]' });
   I.dontSeeCheckboxIsChecked({ css: 'form[name=form1] input[name=first_test_radio]' });
@@ -26,10 +26,10 @@ Scenario('switch iframe manually @WebDriverIO @Puppeteer @Playwright', ({ I }) =
 
 Scenario('within on iframe @WebDriverIO @Puppeteer @Playwright', ({ I }) => {
   I.amOnPage('/iframe');
-  within({ frame: 'iframe' }, () => {
-    I.fillField('rus', 'Updated');
-    I.click('Sign in!');
-    I.waitForText('Email Address');
+  within({ frame: 'iframe' }, async () => {
+    await I.fillField('rus', 'Updated');
+    await I.click('Sign in!');
+    await I.waitForText('Email Address');
   });
   I.see('Iframe test');
   I.dontSee('Email Address');
@@ -37,9 +37,9 @@ Scenario('within on iframe @WebDriverIO @Puppeteer @Playwright', ({ I }) => {
 
 Scenario('within on iframe without iframe navigation @WebDriverIO @Puppeteer @Playwright', ({ I }) => {
   I.amOnPage('/iframe');
-  within({ frame: 'iframe' }, () => {
-    I.fillField('rus', 'Updated');
-    I.see('Sign in!');
+  within({ frame: 'iframe' }, async () => {
+    await I.fillField('rus', 'Updated');
+    await I.see('Sign in!');
   });
   I.see('Iframe test');
   I.dontSee('Sign in!');
@@ -47,9 +47,9 @@ Scenario('within on iframe without iframe navigation @WebDriverIO @Puppeteer @Pl
 
 Scenario('within on nested iframe without iframe navigation depth 2 @WebDriverIO @Puppeteer @Playwright', ({ I }) => {
   I.amOnPage('/iframe_nested');
-  within({ frame: ['[name=wrapper]', '[name=content]'] }, () => {
-    I.fillField('rus', 'Updated');
-    I.see('Sign in!');
+  within({ frame: ['[name=wrapper]', '[name=content]'] }, async () => {
+    await I.fillField('rus', 'Updated');
+    await I.see('Sign in!');
   });
   I.see('Nested Iframe test');
   I.dontSee('Sign in!');
@@ -57,10 +57,10 @@ Scenario('within on nested iframe without iframe navigation depth 2 @WebDriverIO
 
 Scenario('within on nested iframe depth 1 @WebDriverIO @Puppeteer @Playwright', ({ I }) => {
   I.amOnPage('/iframe');
-  within({ frame: ['[name=content]'] }, () => {
-    I.fillField('rus', 'Updated');
-    I.click('Sign in!');
-    I.waitForText('Email Address');
+  within({ frame: ['[name=content]'] }, async () => {
+    await I.fillField('rus', 'Updated');
+    await I.click('Sign in!');
+    await I.waitForText('Email Address');
   });
   I.see('Iframe test');
   I.dontSee('Email Address');
@@ -68,10 +68,10 @@ Scenario('within on nested iframe depth 1 @WebDriverIO @Puppeteer @Playwright',
 
 Scenario('within on nested iframe depth 2 @WebDriverIO @Puppeteer @Playwright', ({ I }) => {
   I.amOnPage('/iframe_nested');
-  within({ frame: ['[name=wrapper]', '[name=content]'] }, () => {
-    I.fillField('rus', 'Updated');
-    I.click('Sign in!');
-    I.see('Email Address');
+  within({ frame: ['[name=wrapper]', '[name=content]'] }, async () => {
+    await I.fillField('rus', 'Updated');
+    await I.click('Sign in!');
+    await I.see('Email Address');
   });
   I.see('Nested Iframe test');
   I.dontSee('Email Address');
@@ -79,10 +79,10 @@ Scenario('within on nested iframe depth 2 @WebDriverIO @Puppeteer @Playwright',
 
 Scenario('within on nested iframe depth 2 and mixed id and xpath selector @WebDriverIO @Puppeteer @Playwright', ({ I }) => {
   I.amOnPage('/iframe_nested');
-  within({ frame: ['#wrapperId', '[name=content]'] }, () => {
-    I.fillField('rus', 'Updated');
-    I.click('Sign in!');
-    I.see('Email Address');
+  within({ frame: ['#wrapperId', '[name=content]'] }, async () => {
+    await I.fillField('rus', 'Updated');
+    await I.click('Sign in!');
+    await I.see('Email Address');
   });
   I.see('Nested Iframe test');
   I.dontSee('Email Address');
@@ -90,10 +90,10 @@ Scenario('within on nested iframe depth 2 and mixed id and xpath selector @WebDr
 
 Scenario('within on nested iframe depth 2 and mixed class and xpath selector @WebDriverIO @Puppeteer @Playwright', ({ I }) => {
   I.amOnPage('/iframe_nested');
-  within({ frame: ['.wrapperClass', '[name=content]'] }, () => {
-    I.fillField('rus', 'Updated');
-    I.click('Sign in!');
-    I.see('Email Address');
+  within({ frame: ['.wrapperClass', '[name=content]'] }, async () => {
+    await I.fillField('rus', 'Updated');
+    await I.click('Sign in!');
+    await I.see('Email Address');
   });
   I.see('Nested Iframe test');
   I.dontSee('Email Address');
@@ -101,7 +101,7 @@ Scenario('within on nested iframe depth 2 and mixed class and xpath selector @We
 
 Scenario('should throw exception if element not found @WebDriverIO @Puppeteer @Playwright', async ({ I }) => {
   I.amOnPage('/form/textarea');
-  within('#grab-multiple', () => {
+  within('#grab-multiple', async () => {
     return I.grabTextFrom('#first-link');
   });
 }).throws(/found/);
diff --git a/test/data/app/view/form/field.php b/test/data/app/view/form/field.php
index 7cf3dea8..e80797bf 100755
--- a/test/data/app/view/form/field.php
+++ b/test/data/app/view/form/field.php
@@ -3,6 +3,7 @@
 <form action="/form/complex" method="POST">
     <label for="name">Name</label>
     <input type="text" id="name" name="name" value="OLD_VALUE" />
+    <div id="textarea" contenteditable>I look like textarea</div>
     <input type="text" id="email" name="email" style="display:none;"/>
     <input type="submit" value="Submit" />
 </form>
diff --git a/test/data/app/view/index.php b/test/data/app/view/index.php
index 25438ed9..08ccb7c0 100755
--- a/test/data/app/view/index.php
+++ b/test/data/app/view/index.php
@@ -3,6 +3,7 @@
 <body>
 
 <h1>Welcome to test app!</h1>
+<h2>With&nbsp;special&nbsp;space chars</h1>
 
 <div class="notice" qa-id = "test"><?php if (isset($notice)) echo $notice; ?></div>
 
diff --git a/test/graphql/GraphQL_test.js b/test/graphql/GraphQL_test.js
index e27f0898..371b951c 100644
--- a/test/graphql/GraphQL_test.js
+++ b/test/graphql/GraphQL_test.js
@@ -136,4 +136,58 @@ describe('GraphQL', () => {
       });
     });
   });
+
+  describe('headers', () => {
+    it('should set headers for all requests multiple times', async () => {
+      I.haveRequestHeaders({ 'XY1-Test': 'xy1-first' });
+      I.haveRequestHeaders({ 'XY1-Test': 'xy1-second' });
+      I.haveRequestHeaders({ 'XY2-Test': 'xy2' });
+
+      const response = await I.sendQuery('{ user(id: 0) { id name email }}');
+
+      response.config.headers.should.have.property('XY1-Test');
+      response.config.headers['XY1-Test'].should.eql('xy1-second');
+
+      response.config.headers.should.have.property('XY2-Test');
+      response.config.headers['XY2-Test'].should.eql('xy2');
+
+      response.config.headers.should.have.property('X-Test');
+      response.config.headers['X-Test'].should.eql('test');
+    });
+
+    it('should override the header set for all requests', async () => {
+      I.haveRequestHeaders({ 'XY-Test': 'first' });
+
+      const response = await I.sendQuery('{ user(id: 0) { id name email }}');
+
+      response.config.headers.should.have.property('XY-Test');
+      response.config.headers['XY-Test'].should.eql('first');
+
+      response.config.headers.should.have.property('X-Test');
+      response.config.headers['X-Test'].should.eql('test');
+    });
+
+    it('should set Bearer authorization', async () => {
+      I.amBearerAuthenticated('token');
+      const response = await I.sendQuery('{ user(id: 0) { id name email }}');
+
+      response.config.headers.should.have.property('Authorization');
+      response.config.headers.Authorization.should.eql('Bearer token');
+
+      response.config.headers.should.have.property('X-Test');
+      response.config.headers['X-Test'].should.eql('test');
+    });
+
+    it('should set Bearer authorization multiple times', async () => {
+      I.amBearerAuthenticated('token1');
+      I.amBearerAuthenticated('token2');
+      const response = await I.sendQuery('{ user(id: 0) { id name email }}');
+
+      response.config.headers.should.have.property('Authorization');
+      response.config.headers.Authorization.should.eql('Bearer token2');
+
+      response.config.headers.should.have.property('X-Test');
+      response.config.headers['X-Test'].should.eql('test');
+    });
+  });
 });
diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js
index 17ae1bf7..fad79e9e 100644
--- a/test/helper/Playwright_test.js
+++ b/test/helper/Playwright_test.js
@@ -305,10 +305,12 @@ describe('Playwright', function () {
   });
 
   describe('#switchTo', () => {
-    it('should switch reference to iframe content', () => I.amOnPage('/iframe')
-      .then(() => I.switchTo('[name="content"]'))
-      .then(() => I.see('Information'))
-      .then(() => I.see('Lots of valuable data here')));
+    it('should switch reference to iframe content', () => {
+      I.amOnPage('/iframe');
+      I.switchTo('[name="content"]');
+      I.see('Information');
+      I.see('Lots of valuable data here');
+    });
 
     it('should return error if iframe selector is invalid', () => I.amOnPage('/iframe')
       .then(() => I.switchTo('#invalidIframeSelector'))
@@ -324,12 +326,14 @@ describe('Playwright', function () {
         e.message.should.be.equal('Element "#invalidIframeSelector" was not found by text|CSS|XPath');
       }));
 
-    it('should return to parent frame given a null locator', () => I.amOnPage('/iframe')
-      .then(() => I.switchTo('[name="content"]'))
-      .then(() => I.see('Information'))
-      .then(() => I.see('Lots of valuable data here'))
-      .then(() => I.switchTo(null))
-      .then(() => I.see('Iframe test')));
+    it('should return to parent frame given a null locator', async () => {
+      I.amOnPage('/iframe');
+      I.switchTo('[name="content"]');
+      I.see('Information');
+      I.see('Lots of valuable data here');
+      I.switchTo(null);
+      I.see('Iframe test');
+    });
   });
 
   describe('#seeInSource, #grabSource', () => {
@@ -462,6 +466,12 @@ describe('Playwright', function () {
       await I.dontSeeInField('Name', 'value that is cleared using I.clearField()');
     });
 
+    it('should clear div textarea', async () => {
+      await I.amOnPage('/form/field');
+      await I.clearField('#textarea');
+      await I.dontSeeInField('#textarea', 'I look like textarea');
+    });
+
     it('should clear textarea', async () => {
       await I.amOnPage('/form/textarea');
       await I.fillField('#description', 'value that is cleared using I.clearField()');
diff --git a/test/helper/webapi.js b/test/helper/webapi.js
index 6bfdb53f..3e9959ce 100644
--- a/test/helper/webapi.js
+++ b/test/helper/webapi.js
@@ -113,6 +113,12 @@ module.exports.tests = function () {
       await I.see("Don't do that at home!", 'h3');
       await I.see('Текст', 'p');
     });
+
+    it('should verify text with &nbsp', async () => {
+      if (isHelper('TestCafe') || isHelper('WebDriver')) return;
+      await I.amOnPage('/');
+      await I.see('With special space chars');
+    });
   });
 
   describe('see element : #seeElement, #seeElementInDOM, #dontSeeElement', () => {
@@ -1104,10 +1110,10 @@ module.exports.tests = function () {
     it('should execute within block', async () => {
       await I.amOnPage('/form/example4');
       await I.seeElement('#navbar-collapse-menu');
-      await I._withinBegin('#register');
-      await I.see('E-Mail');
-      await I.dontSee('Toggle navigation');
-      await I.dontSeeElement('#navbar-collapse-menu');
+      I._withinBegin('#register')
+        .then(() => I.see('E-Mail'))
+        .then(() => I.dontSee('Toggle navigation'))
+        .then(() => I.dontSeeElement('#navbar-collapse-menu'));
     });
 
     it('should respect form fields inside within block ', async () => {
@@ -1121,15 +1127,10 @@ module.exports.tests = function () {
       await I.seeInField('Hasło', '12345');
       await I.checkOption('terms');
       await I.seeCheckboxIsChecked('terms');
-      await I._withinBegin({ css: '.form-group' });
-      await I.see('E-Mail');
-      await I.dontSee('Hasło');
-
-      try {
-        await I.dontSeeElement('#navbar-collapse-menu');
-      } catch (err) {
-        rethrow = err;
-      }
+      I._withinBegin({ css: '.form-group' })
+        .then(() => I.see('E-Mail'))
+        .then(() => I.dontSee('Hasło'))
+        .then(() => I.dontSeeElement('#navbar-collapse-menu'));
 
       try {
         await I.dontSeeCheckboxIsChecked('terms');
@@ -1149,15 +1150,13 @@ module.exports.tests = function () {
     it('should execute within block 2', async () => {
       await I.amOnPage('/form/example4');
       await I.fillField('Hasło', '12345');
-      await I._withinBegin({ xpath: '//div[@class="form-group"][2]' });
-      await I.dontSee('E-Mail');
-      await I.see('Hasło');
-
-      const label = await I.grabTextFrom('label');
-      assert.equal(label, 'Hasło');
-
-      const input = await I.grabValueFrom('input');
-      assert.equal(input, '12345');
+      I._withinBegin({ xpath: '//div[@class="form-group"][2]' })
+        .then(() => I.dontSee('E-Mail'))
+        .then(() => I.see('Hasło'))
+        .then(() => I.grabTextFrom('label'))
+        .then((label) => assert.equal(label, 'Hasło'))
+        .then(() => I.grabValueFrom('input'))
+        .then((input) => assert.equal(input, '12345'));
     });
 
     it('within should respect context in see', async function () {
@@ -1165,19 +1164,9 @@ module.exports.tests = function () {
 
       await I.amOnPage('/form/example4');
       await I.see('Rejestracja', 'fieldset');
-      await I._withinBegin({ css: '.navbar-header' });
-
-      try {
-        await I.see('Rejestracja', '.container fieldset');
-      } catch (err) {
-        if (!err) assert.fail('seen fieldset');
-      }
-
-      try {
-        await I.see('Toggle navigation', '.container fieldset');
-      } catch (err) {
-        if (!err) assert.fail('seen fieldset');
-      }
+      I._withinBegin({ css: '.navbar-header' })
+        .then(() => I.see('Rejestracja', '.container fieldset'))
+        .then(() => I.see('Toggle navigation', '.container fieldset'));
     });
 
     it('within should respect context in see when using nested frames', async function () {
diff --git a/test/runner/dry_run_test.js b/test/runner/dry_run_test.js
index d3faa2f3..ac4f1015 100644
--- a/test/runner/dry_run_test.js
+++ b/test/runner/dry_run_test.js
@@ -81,10 +81,10 @@ describe('dry-run command', () => {
       expect(lines).toEqual(
         expect.arrayContaining([
           '  check current dir',
-          '    I: openDir "aaa"',
+          '    I open dir "aaa"',
           '      I am in path "."',
           '      I see file "codecept.class.js"',
-          '    MyPage: hasFile "First arg", "Second arg"',
+          '    On MyPage: has file "First arg", "Second arg"',
           '      I see file "codecept.class.js"',
           '      I see file "codecept.po.js"',
           '    I see file "codecept.po.js"',
@@ -150,10 +150,10 @@ describe('dry-run command', () => {
       expect(lines).toEqual(
         expect.arrayContaining([
           '  check current dir',
-          '    I: openDir "aaa"',
+          '    I open dir "aaa"',
           '      I am in path "."',
           '      I see file "codecept.class.js"',
-          '    MyPage: hasFile "uu"',
+          '    On MyPage: has file "uu"',
           '      I see file "codecept.class.js"',
           '      I see file "codecept.po.js"',
           '    I see file "codecept.po.js"',
diff --git a/test/runner/pageobject_test.js b/test/runner/pageobject_test.js
index 913ad5a6..6e537b72 100644
--- a/test/runner/pageobject_test.js
+++ b/test/runner/pageobject_test.js
@@ -36,9 +36,9 @@ describe('CodeceptJS PageObject', () => {
     it('should inject page objects by class', (done) => {
       exec(`${config_run_config('codecept.class.js', '@ClassPageObject')} --debug`, (err, stdout) => {
         expect(stdout).not.toContain('classpage.type is not a function');
-        expect(stdout).toContain('classpage: type "Class Page Type"');
+        expect(stdout).toContain('On classpage: type "Class Page Type"');
         expect(stdout).toContain('I print message "Class Page Type"');
-        expect(stdout).toContain('classpage: purgeDomains');
+        expect(stdout).toContain('On classpage: purge domains');
         expect(stdout).toContain('I print message "purgeDomains"');
         expect(stdout).toContain('Class Page Type');
         expect(stdout).toContain('OK  | 1 passed');
@@ -50,10 +50,10 @@ describe('CodeceptJS PageObject', () => {
     it('should inject page objects by class which nested base clas', (done) => {
       exec(`${config_run_config('codecept.class.js', '@NestedClassPageObject')} --debug`, (err, stdout) => {
         expect(stdout).not.toContain('classnestedpage.type is not a function');
-        expect(stdout).toContain('classnestedpage: type "Nested Class Page Type"');
+        expect(stdout).toContain('On classnestedpage: type "Nested Class Page Type"');
         expect(stdout).toContain('user => User1');
         expect(stdout).toContain('I print message "Nested Class Page Type"');
-        expect(stdout).toContain('classnestedpage: purgeDomains');
+        expect(stdout).toContain('On classnestedpage: purge domains');
         expect(stdout).toContain('I print message "purgeDomains"');
         expect(stdout).toContain('Nested Class Page Type');
         expect(stdout).toContain('OK  | 1 passed');
@@ -89,10 +89,10 @@ describe('CodeceptJS PageObject', () => {
         expect(lines).toEqual(
           expect.arrayContaining([
             '  check current dir',
-            '    I: openDir "aaa"',
+            '    I open dir "aaa"',
             '      I am in path "."',
             '      I see file "codecept.class.js"',
-            '    MyPage: hasFile "First arg", "Second arg"',
+            '    On MyPage: has file "First arg", "Second arg"',
             '      I see file "codecept.class.js"',
             '      I see file "codecept.po.js"',
             '    I see file "codecept.po.js"',
@@ -113,10 +113,10 @@ describe('CodeceptJS PageObject', () => {
         expect(lines).toEqual(
           expect.arrayContaining([
             '  check current dir',
-            '    I: openDir "aaa"',
+            '    I open dir "aaa"',
             '      I am in path "."',
             '      I see file "codecept.class.js"',
-            '    MyPage: hasFile "uu"',
+            '    On MyPage: has file "uu"',
             '      I see file "codecept.class.js"',
             '      I see file "codecept.po.js"',
             '    I see file "codecept.po.js"',
@@ -136,10 +136,10 @@ describe('CodeceptJS PageObject', () => {
         expect(lines).toEqual(
           expect.arrayContaining([
             '  pageobject with context',
-            '    I: openDir "aaa"',
+            '    I open dir "aaa"',
             '      I am in path "."',
             '      I see file "codecept.class.js"',
-            '    MyPage: hasFile "uu"',
+            '    On MyPage: has file "uu"',
             '      I see file "codecept.class.js"',
             '      I see file "codecept.po.js"',
             '    I see file "codecept.po.js"',
diff --git a/test/runner/within_test.js b/test/runner/within_test.js
index f1d74c47..430ed77a 100644
--- a/test/runner/within_test.js
+++ b/test/runner/within_test.js
@@ -24,12 +24,12 @@ describe('CodeceptJS within', function () {
       testStatus.should.include('OK');
       withoutGeneratorList.should.eql([
         'I small promise ',
-        'I small  Promise was finished ',
-        'I  Hey!  I am within  Begin.  I get blabla ',
-        'Within "blabla" ',
+        'I small promise was finished ',
+        'I hey! i am within begin. i get blabla ',
+        'Within "blabla" ""',
         'I small promise ',
-        'I small  Promise was finished ',
-        'I oh!  I am within end( ',
+        'I small promise was finished ',
+        'I oh! i am within end( ',
       ], 'check steps execution order');
       done();
     });
@@ -44,16 +44,16 @@ describe('CodeceptJS within', function () {
       testStatus.should.include('OK');
       withGeneratorList.should.eql([
         'I small promise ',
-        'I small  Promise was finished ',
+        'I small promise was finished ',
         'I small yield ',
         'I am small yield string await',
-        'I  Hey!  I am within  Begin.  I get blabla ',
-        'Within "blabla" ',
+        'I hey! i am within begin. i get blabla ',
+        'Within "blabla" ""',
         'I small yield ',
         'I am small yield string await',
         'I small promise ',
-        'I small  Promise was finished ',
-        'I oh!  I am within end( ',
+        'I small promise was finished ',
+        'I oh! i am within end( ',
       ], 'check steps execution order');
 
       done();
@@ -69,16 +69,16 @@ describe('CodeceptJS within', function () {
       testStatus.should.include('OK');
       withGeneratorList.should.eql([
         'I small promise ',
-        'I small  Promise was finished ',
+        'I small promise was finished ',
         'I small yield ',
         'I am small yield string await',
-        'I  Hey!  I am within  Begin.  I get blabla ',
-        'Within "blabla" ',
+        'I hey! i am within begin. i get blabla ',
+        'Within "blabla" ""',
         'I small promise ',
-        'I small  Promise was finished ',
+        'I small promise was finished ',
         'I small yield ',
         'I am small yield string await',
-        'I oh!  I am within end( ',
+        'I oh! i am within end( ',
       ], 'check steps execution order');
 
       done();
diff --git a/test/unit/steps_test.js b/test/unit/steps_test.js
index 497ac8a2..9c45e968 100644
--- a/test/unit/steps_test.js
+++ b/test/unit/steps_test.js
@@ -111,7 +111,12 @@ describe('Steps', () => {
 
       it('should correct print step info for simple PageObject', () => {
         const metaStep = new MetaStep('MyPage', 'clickByName');
-        expect(metaStep.toString()).to.include('MyPage: clickByName');
+        expect(metaStep.toString()).to.include('On MyPage: click by name');
+      });
+
+      it('should correct print step info for custom step', () => {
+        const metaStep = new MetaStep('I', 'clickByName');
+        expect(metaStep.toString()).to.include('I click by name');
       });
 
       it('should correct print step with args', () => {
@@ -120,7 +125,7 @@ describe('Steps', () => {
         const msg2 = 'second message';
         const fn = (msg) => `result from callback = ${msg}`;
         metaStep.run.bind(metaStep, fn)(msg, msg2);
-        expect(metaStep.toString()).eql(`MyPage: clickByName "${msg}", "${msg2}"`);
+        expect(metaStep.toString()).eql(`On MyPage: click by name "${msg}", "${msg2}"`);
       });
     });