Repository: codeceptjs/CodeceptJS (js)

Issue:
flags are not supported in Regex for cucumber steps
#### What are you trying to achieve?
**Create a cucumber step using regex**
Example Step: _Given(/^I am on "([^"]+)" page$/i, openPageStep);_

#### What do you get instead?
Error: No steps matching "I am on "Home" page"
> Provide console output if related. Use `--verbose` mode for more details.

```bash
# No steps matching "I am on "Home" page"
```

### Details
If you omit the flag **i** in the regex expression then it is working fine but the issue occurs only if you introduce any [flag](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#advanced_searching_with_flags)

* CodeceptJS version: 3.1.1
* NodeJS Version: v16.13.1
* Operating System: Windows


Pull request:
Handle flags of Regex expression for cucumber
## Motivation/Description of the PR
- Fixed issue - Handle flags of Regex expression for cucumber step definition.
- Resolves #3212.

## Current Issue
If you create a step definition like ``` Given(/^I am on "([^"]+)" page$/i, openPageStep); ``` and use ``` Given I am on "Login" page```, you will get error stating ````# No steps matching "I am on "Login" page"```` but It will work for ``` Given(/^I am on "([^"]+)" page$/, openPageStep); ```

## Type of change

- [ ] :fire: Breaking changes
- [ ] :rocket: New functionality
- [x] :bug: Bug fix
- [ ] :clipboard: Documentation changes/updates
- [ ] :hotsprings: Hot fix
- [ ] :hammer: Markdown files fix - not related to source code
- [ ] :nail_care: Polish code

## Checklist:

- [x] Tests have been added
- [ ] Documentation has been added (Run `npm run docs`)
- [x] Lint checking (Run `npm run lint`)
- [x] Local tests are passed (Run `npm test`)


Patch:
diff --git a/docs/playwright.md b/docs/playwright.md
index 9f4f48b0..bed3790f 100644
--- a/docs/playwright.md
+++ b/docs/playwright.md
@@ -175,8 +175,10 @@ If you need to get element's value inside a test you can use `grab*` methods. Th
 ```js
 const assert = require('assert');
 Scenario('get value of current tasks', async ({ I }) => {
-  I.createTodo('do 1');
-  I.createTodo('do 2');
+  I.fillField('.todo', 'my first item');
+  I.pressKey('Enter')
+  I.fillField('.todo', 'my second item');
+  I.pressKey('Enter')
   let numTodos = await I.grabTextFrom('.todo-count strong');
   assert.equal(2, numTodos);
 });
@@ -188,17 +190,35 @@ In case some actions should be taken inside one element (a container or modal wi
 Please take a note that you can't use within inside another within in Playwright helper:
 
 ```js
-within('.todoapp', () => {
-  I.createTodo('my new item');
+await within('.todoapp', () => {
+  I.fillField('.todo', 'my new item');
+  I.pressKey('Enter')
   I.see('1 item left', '.todo-count');
   I.click('.todo-list input.toggle');
 });
 I.see('0 items left', '.todo-count');
 ```
 
-> [▶ Learn more about basic commands](/basics#writing-tests)
+### Each Element <Badge text="Since 3.3" type="warning"/>
 
-CodeceptJS allows you to implement custom actions like `I.createTodo` or use **PageObjects**. Learn how to improve your tests in [PageObjects](https://codecept.io/pageobjects/) guide.
+Usually, CodeceptJS performs an action on the first matched element. 
+In case you want to do an action on each element found, use the special function `eachElement` which comes from [eachElement](https://codecept.io/plugins/#eachelement) plugin. 
+
+`eachElement` function matches all elements by locator and performs a callback on each of those element. A callback function receives [ElementHandle instance](https://playwright.dev/docs/api/class-elementhandle) from Playwright API. `eachElement` may perform arbitrary actions on a page, so the first argument should by a description of the actions performed. This description will be used for logging purposes.
+
+Usage example
+
+```js
+await eachElement(
+  'tick all checkboxes', 
+  'input.custom-checkbox', 
+  async (el, index) => {
+    await el.check();
+  });
+);
+```
+
+> ℹ Learn more about [eachElement plugin](/plugins/#eachelement)
 
 ## Multi Session Testing
 
@@ -307,19 +327,6 @@ Scenario('website looks nice on iPhone', () => {
 });
 ```
 
-## Configuring CI
-
-### GitHub Actions
-
-Playwright can be added to GitHub Actions using [official action](https://github.com/microsoft/playwright-github-action). Use it before starting CodeceptJS tests to install all dependencies. It is important to run tests in headless mode ([otherwise you will need to enable xvfb to emulate desktop](https://github.com/microsoft/playwright-github-action#run-in-headful-mode)).
-
-```yml
-# from workflows/tests.yml
-- uses: microsoft/playwright-github-action@v1
-- name: run CodeceptJS tests
-  run: npx codeceptjs run
-```
-
 ## Accessing Playwright API
 
 To get [Playwright API](https://playwright.dev/docs/api/class-playwright) inside a test use `I.usePlaywrightTo` method with a callback.
@@ -496,6 +503,7 @@ Open `index.html` in your browser to view the full interactive coverage report.
 ![](https://user-images.githubusercontent.com/16587779/131858993-87d1aafc-8ef1-4a82-867d-e64a13e36106.png)
 
 ![](https://user-images.githubusercontent.com/16587779/131859006-c6f17d18-c603-44a5-9d59-0670177276cf.png)
+
 ## Extending Helper
 
 To create custom `I.*` commands using Playwright API you need to create a custom helper.
@@ -538,3 +546,17 @@ async setPermissions() {
 > [▶ Learn more about BrowserContext](https://github.com/microsoft/playwright/blob/master/docs/src/api/class-browsercontext.md)
 
 > [▶ Learn more about Helpers](https://codecept.io/helpers/)
+
+
+## Configuring CI
+
+### GitHub Actions
+
+Playwright can be added to GitHub Actions using [official action](https://github.com/microsoft/playwright-github-action). Use it before starting CodeceptJS tests to install all dependencies. It is important to run tests in headless mode ([otherwise you will need to enable xvfb to emulate desktop](https://github.com/microsoft/playwright-github-action#run-in-headful-mode)).
+
+```yml
+# from workflows/tests.yml
+- uses: microsoft/playwright-github-action@v1
+- name: run CodeceptJS tests
+  run: npx codeceptjs run
+```
\ No newline at end of file
diff --git a/docs/plugins.md b/docs/plugins.md
index 28365c3b..c3f0d26b 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -501,11 +501,76 @@ I.click('=sign-up'); // matches => [data-qa=sign-up]
 
 -   `config`  
 
+## eachElement
+
+Provides `eachElement` global function to iterate over found elements to perform actions on them.
+
+`eachElement` takes following args:
+
+-   `purpose` - the goal of an action. A comment text that will be displayed in output.
+-   `locator` - a CSS/XPath locator to match elements
+-   `fn(element, index)` - **asynchronous** function which will be executed for each matched element.
+
+Example of usage:
+
+```js
+// this example works with Playwright and Puppeteer helper
+await eachElement('click all checkboxes', 'form input[type=checkbox]', async (el) => {
+  await el.click();
+});
+```
+
+Click odd elements:
+
+```js
+// this example works with Playwright and Puppeteer helper
+await eachElement('click odd buttons', '.button-select', async (el, index) => {
+  if (index % 2) await el.click();
+});
+```
+
+Check all elements for visibility:
+
+```js
+// this example works with Playwright and Puppeteer helper
+const assert = require('assert');
+await eachElement('check all items are visible', '.item', async (el) => {
+  assert(await el.isVisible());
+});
+```
+
+This method works with WebDriver, Playwright, Puppeteer, Appium helpers.
+
+Function parameter `el` represents a matched element.
+Depending on a helper API of `el` can be different. Refer to API of corresponding browser testing engine for a complete API list:
+
+-   [Playwright ElementHandle][4]
+-   [Puppeteer][5]
+-   [webdriverio element][6]
+
+#### Configuration
+
+-   `registerGlobal` - to register `eachElement` function globally, true by default
+
+If `registerGlobal` is false you can use eachElement from the plugin:
+
+```js
+const eachElement = codeceptjs.container.plugins('eachElement');
+```
+
+### Parameters
+
+-   `purpose` **[string][7]** 
+-   `locator` **CodeceptJS.LocatorOrString** 
+-   `fn` **[Function][8]** 
+
+Returns **([Promise][9]&lt;any> | [undefined][10])** 
+
 ## fakerTransform
 
-Use the [faker.js][4] package to generate fake data inside examples on your gherkin tests
+Use the [faker.js][11] package to generate fake data inside examples on your gherkin tests
 
-![Faker.js][5]
+![Faker.js][12]
 
 #### Usage
 
@@ -543,7 +608,7 @@ Scenario Outline: ...
 
 ## pauseOnFail
 
-Automatically launches [interactive pause][6] when a test fails.
+Automatically launches [interactive pause][13] when a test fails.
 
 Useful for debugging flaky tests on local environment.
 Add this plugin to config file:
@@ -726,14 +791,14 @@ Possible config options:
 
 ## selenoid
 
-[Selenoid][7] plugin automatically starts browsers and video recording.
+[Selenoid][14] plugin automatically starts browsers and video recording.
 Works with WebDriver helper.
 
 ### Prerequisite
 
 This plugin **requires Docker** to be installed.
 
-> If you have issues starting Selenoid with this plugin consider using the official [Configuration Manager][8] tool from Selenoid
+> If you have issues starting Selenoid with this plugin consider using the official [Configuration Manager][15] tool from Selenoid
 
 ### Usage
 
@@ -762,7 +827,7 @@ plugins: {
   }
 ```
 
-When `autoCreate` is enabled it will pull the [latest Selenoid from DockerHub][9] and start Selenoid automatically.
+When `autoCreate` is enabled it will pull the [latest Selenoid from DockerHub][16] and start Selenoid automatically.
 It will also create `browsers.json` file required by Selenoid.
 
 In automatic mode the latest version of browser will be used for tests. It is recommended to specify exact version of each browser inside `browsers.json` file.
@@ -774,10 +839,10 @@ In automatic mode the latest version of browser will be used for tests. It is re
 While this plugin can create containers for you for better control it is recommended to create and launch containers manually.
 This is especially useful for Continous Integration server as you can configure scaling for Selenoid containers.
 
-> Use [Selenoid Configuration Manager][8] to create and start containers semi-automatically.
+> Use [Selenoid Configuration Manager][15] to create and start containers semi-automatically.
 
 1.  Create `browsers.json` file in the same directory `codecept.conf.js` is located
-    [Refer to Selenoid documentation][10] to know more about browsers.json.
+    [Refer to Selenoid documentation][17] to know more about browsers.json.
 
 _Sample browsers.json_
 
@@ -802,7 +867,7 @@ _Sample browsers.json_
 
 2.  Create Selenoid container
 
-Run the following command to create a container. To know more [refer here][11]
+Run the following command to create a container. To know more [refer here][18]
 
 ```bash
 docker create                                    \
@@ -835,7 +900,7 @@ When `allure` plugin is enabled a video is attached to report automatically.
 | enableVideo      | Enable video recording and use `video` folder of output (default: false)       |
 | enableLog        | Enable log recording and use `logs` folder of output (default: false)          |
 | deletePassed     | Delete video and logs of passed tests (default : true)                         |
-| additionalParams | example: `additionalParams: '--env TEST=test'` [Refer here][12] to know more   |
+| additionalParams | example: `additionalParams: '--env TEST=test'` [Refer here][19] to know more   |
 
 ### Parameters
 
@@ -843,7 +908,7 @@ When `allure` plugin is enabled a video is attached to report automatically.
 
 ## stepByStepReport
 
-![step-by-step-report][13]
+![step-by-step-report][20]
 
 Generates step by step report for a test.
 After each step in a test a screenshot is created. After test executed screenshots are combined into slideshow.
@@ -1024,7 +1089,7 @@ This plugin allows to run webdriverio services like:
 -   browserstack
 -   appium
 
-A complete list of all available services can be found on [webdriverio website][14].
+A complete list of all available services can be found on [webdriverio website][21].
 
 #### Setup
 
@@ -1036,7 +1101,7 @@ See examples below:
 
 #### Selenium Standalone Service
 
-Install `@wdio/selenium-standalone-service` package, as [described here][15].
+Install `@wdio/selenium-standalone-service` package, as [described here][22].
 It is important to make sure it is compatible with current webdriverio version.
 
 Enable `wdio` plugin in plugins list and add `selenium-standalone` service:
@@ -1055,7 +1120,7 @@ Please note, this service can be used with Protractor helper as well!
 
 #### Sauce Service
 
-Install `@wdio/sauce-service` package, as [described here][16].
+Install `@wdio/sauce-service` package, as [described here][23].
 It is important to make sure it is compatible with current webdriverio version.
 
 Enable `wdio` plugin in plugins list and add `sauce` service:
@@ -1091,28 +1156,42 @@ In the same manner additional services from webdriverio can be installed, enable
 
 [3]: https://codecept.io/locators#custom-locators
 
-[4]: https://www.npmjs.com/package/faker
+[4]: https://playwright.dev/docs/api/class-elementhandle
+
+[5]: https://pptr.dev/#?product=Puppeteer&show=api-class-elementhandle
+
+[6]: https://webdriver.io/docs/api
+
+[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
+
+[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
+
+[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise
+
+[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
+
+[11]: https://www.npmjs.com/package/faker
 
-[5]: https://raw.githubusercontent.com/Marak/faker.js/master/logo.png
+[12]: https://raw.githubusercontent.com/Marak/faker.js/master/logo.png
 
-[6]: /basics/#pause
+[13]: /basics/#pause
 
-[7]: https://aerokube.com/selenoid/
+[14]: https://aerokube.com/selenoid/
 
-[8]: https://aerokube.com/cm/latest/
+[15]: https://aerokube.com/cm/latest/
 
-[9]: https://hub.docker.com/u/selenoid
+[16]: https://hub.docker.com/u/selenoid
 
-[10]: https://aerokube.com/selenoid/latest/#_prepare_configuration
+[17]: https://aerokube.com/selenoid/latest/#_prepare_configuration
 
-[11]: https://aerokube.com/selenoid/latest/#_option_2_start_selenoid_container
+[18]: https://aerokube.com/selenoid/latest/#_option_2_start_selenoid_container
 
-[12]: https://docs.docker.com/engine/reference/commandline/create/
+[19]: https://docs.docker.com/engine/reference/commandline/create/
 
-[13]: https://codecept.io/img/codeceptjs-slideshow.gif
+[20]: https://codecept.io/img/codeceptjs-slideshow.gif
 
-[14]: https://webdriver.io
+[21]: https://webdriver.io
 
-[15]: https://webdriver.io/docs/selenium-standalone-service.html
+[22]: https://webdriver.io/docs/selenium-standalone-service.html
 
-[16]: https://webdriver.io/docs/sauce-service.html
+[23]: https://webdriver.io/docs/sauce-service.html
diff --git a/docs/puppeteer.md b/docs/puppeteer.md
index c3d54e7b..f0fec00e 100644
--- a/docs/puppeteer.md
+++ b/docs/puppeteer.md
@@ -110,7 +110,6 @@ Tests consist with a scenario of user's action taken on a page. The most widely
 All actions which interact with elements **support CSS and XPath locators**. Actions like `click` or `fillField` by locate elements by their name or value on a page:
 
 ```js
-
 // search for link or button
 I.click('Login');
 // locate field by its label
@@ -172,8 +171,10 @@ If you need to get element's value inside a test you can use `grab*` methods. Th
 ```js
 const assert = require('assert');
 Scenario('get value of current tasks', async ({ I }) => {
-  I.createTodo('do 1');
-  I.createTodo('do 2');
+  I.fillField('.todo', 'my first item');
+  I.pressKey('Enter')
+  I.fillField('.todo', 'my second item');
+  I.pressKey('Enter')
   let numTodos = await I.grabTextFrom('.todo-count strong');
   assert.equal(2, numTodos);
 });
@@ -185,20 +186,35 @@ In case some actions should be taken inside one element (a container or modal wi
 Please take a note that you can't use within inside another within in Puppeteer helper:
 
 ```js
-within('.todoapp', () => {
-  I.createTodo('my new item');
+await within('.todoapp', () => {
+  I.fillField('.todo', 'my new item');
+  I.pressKey('Enter')
   I.see('1 item left', '.todo-count');
   I.click('.todo-list input.toggle');
 });
 I.see('0 items left', '.todo-count');
 ```
 
-> [▶ Learn more about basic commands](/basics#writing-tests)
+### Each Element <Badge text="Since 3.3" type="warning"/>
+
+Usually, CodeceptJS performs an action on the first matched element. 
+In case you want to do an action on each element found, use the special function `eachElement` which comes from [eachElement](https://codecept.io/plugins/#eachelement) plugin. 
+
+`eachElement` function matches all elements by locator and performs a callback on each of those element. A callback function receives [ElementHandle instance](https://pptr.dev/#?product=Puppeteer&show=api-class-elementhandle) from Puppeteer API. `eachElement` may perform arbitrary actions on a page, so the first argument should by a description of the actions performed. This description will be used for logging purposes.
 
-CodeceptJS allows you to implement custom actions like `I.createTodo` or use **PageObjects**. Learn how to improve your tests in [PageObjects](https://codecept.io/pageobjects/) guide.
+Usage example
 
-> [▶ Demo project is available on GitHub](https://github.com/DavertMik/codeceptjs-todomvc-puppeteer)
+```js
+await eachElement(
+  'click all checkboxes', 
+  'input.custom-checkbox', 
+  async (el, index) => {
+    await el.click();
+  });
+);
+```
 
+> ℹ Learn more about [eachElement plugin](/plugins/#eachelement)
 
 ## Mocking Requests
 
diff --git a/docs/webdriver.md b/docs/webdriver.md
index de2625ba..232da57e 100644
--- a/docs/webdriver.md
+++ b/docs/webdriver.md
@@ -203,8 +203,6 @@ Scenario('login test', ({ I }) => {
   I.see('Welcome, John');
 });
 ```
-> ▶ Actions like `amOnPage`, `click`, `fillField` are not limited to WebDriver only. They work similarly for all available helpers. [Go to Basics guide to learn them](/basics#writing-tests).
-
 
 An empty test case can be created with `npx codeceptjs gt` command.
 
@@ -212,6 +210,43 @@ An empty test case can be created with `npx codeceptjs gt` command.
 npx codeceptjs gt
 ```
 
+
+### Actions
+
+Tests consist with a scenario of user's action taken on a page. The most widely used ones are:
+
+* `amOnPage` - to open a webpage (accepts relative or absolute url)
+* `click` - to locate a button or link and click on it
+* `fillField` - to enter a text inside a field
+* `selectOption`, `checkOption` - to interact with a form
+* `wait*` to wait for some parts of page to be fully rendered (important for testing SPA)
+* `grab*` to get values from page sources
+* `see`, `dontSee` - to check for a text on a page
+* `seeElement`, `dontSeeElement` - to check for elements on a page
+
+> ℹ  All actions are listed in [WebDriver helper reference](https://codecept.io/helpers/WebDriver/).*
+
+All actions which interact with elements **support CSS and XPath locators**. Actions like `click` or `fillField` by locate elements by their name or value on a page:
+
+```js
+// search for link or button
+I.click('Login');
+// locate field by its label
+I.fillField('Name', 'Miles');
+// we can use input name
+I.fillField('user[email]','miles@davis.com');
+```
+
+You can also specify the exact locator type with strict locators:
+
+```js
+I.click({css: 'button.red'});
+I.fillField({name: 'user[email]'},'miles@davis.com');
+I.seeElement({xpath: '//body/header'});
+```
+
+### Interactive Pause
+
 It's easy to start writing a test if you use [interactive pause](/basics#debug). Just open a web page and pause execution.
 
 ```js
@@ -268,6 +303,59 @@ Scenario('create todo item', ({ I }) => {
 
 WebDriver helper supports standard [CSS/XPath and text locators](/locators) as well as non-trivial [React locators](/react) and [Shadow DOM](/shadow).
 
+### Grabbers
+
+If you need to get element's value inside a test you can use `grab*` methods. They should be used with `await` operator inside `async` function:
+
+```js
+const assert = require('assert');
+Scenario('get value of current tasks', async ({ I }) => {
+  I.fillField('.todo', 'my first item');
+  I.pressKey('Enter')
+  I.fillField('.todo', 'my second item');
+  I.pressKey('Enter')
+  let numTodos = await I.grabTextFrom('.todo-count strong');
+  assert.equal(2, numTodos);
+});
+```
+
+### Within
+
+In case some actions should be taken inside one element (a container or modal window or iframe) you can use `within` block to narrow the scope.
+Please take a note that you can't use within inside another within in Puppeteer helper:
+
+```js
+await within('.todoapp', () => {
+  I.fillField('.todo', 'my new item');
+  I.pressKey('Enter')
+  I.see('1 item left', '.todo-count');
+  I.click('.todo-list input.toggle');
+});
+I.see('0 items left', '.todo-count');
+```
+
+### Each Element <Badge text="Since 3.3" type="warning"/>
+
+Usually, CodeceptJS performs an action on the first matched element. 
+In case you want to do an action on each element found, use the special function `eachElement` which comes from [eachElement](https://codecept.io/plugins/#eachelement) plugin. 
+
+`eachElement` function matches all elements by locator and performs a callback on each of those element. A callback function receives element of webdriverio. `eachElement` may perform arbitrary actions on a page, so the first argument should by a description of the actions performed. This description will be used for logging purposes.
+
+Usage example
+
+```js
+await eachElement(
+  'click all checkboxes', 
+  'input.custom-checkbox', 
+  async (el, index) => {
+    await el.click();
+  });
+);
+```
+
+> ℹ Learn more about [eachElement plugin](/plugins/#eachelement)
+
+
 ## Waiting
 
 Web applications do not always respond instantly. That's why WebDriver protocol has methods to wait for changes on a page. CodeceptJS provides such commands prefixed with `wait*` so you could explicitly define what effects we wait for.
diff --git a/lib/interfaces/bdd.js b/lib/interfaces/bdd.js
index 8da0e542..81d8de4e 100644
--- a/lib/interfaces/bdd.js
+++ b/lib/interfaces/bdd.js
@@ -25,7 +25,8 @@ const parameterTypeRegistry = new ParameterTypeRegistry();
 const matchStep = (step) => {
   for (const stepName in steps) {
     if (stepName.indexOf('/') === 0) {
-      const res = step.match(new RegExp(stepName.slice(1, -1)));
+      const regExpArr = stepName.match(new RegExp('^/(.*?)/([gimy]*)$')) || [];
+      const res = step.match(new RegExp(regExpArr[1], regExpArr[2]));
       if (res) {
         const fn = steps[stepName];
         fn.params = res.slice(1);
@@ -56,6 +57,7 @@ module.exports = {
   Given: addStep,
   When: addStep,
   Then: addStep,
+  And: addStep,
   matchStep,
   getSteps,
   clearSteps,
diff --git a/lib/plugin/eachElement.js b/lib/plugin/eachElement.js
new file mode 100644
index 00000000..14e20a3a
--- /dev/null
+++ b/lib/plugin/eachElement.js
@@ -0,0 +1,127 @@
+const output = require('../output');
+const store = require('../store');
+const recorder = require('../recorder');
+const container = require('../container');
+const event = require('../event');
+const Step = require('../step');
+const { isAsyncFunction } = require('../utils');
+
+const defaultConfig = {
+  registerGlobal: true,
+};
+
+/**
+ * Provides `eachElement` global function to iterate over found elements to perform actions on them.
+ *
+ * `eachElement` takes following args:
+ * * `purpose` - the goal of an action. A comment text that will be displayed in output.
+ * * `locator` - a CSS/XPath locator to match elements
+ * * `fn(element, index)` - **asynchronous** function which will be executed for each matched element.
+ *
+ * Example of usage:
+ *
+ * ```js
+ * // this example works with Playwright and Puppeteer helper
+ * await eachElement('click all checkboxes', 'form input[type=checkbox]', async (el) => {
+ *   await el.click();
+ * });
+ * ```
+ * Click odd elements:
+ *
+ * ```js
+ * // this example works with Playwright and Puppeteer helper
+ * await eachElement('click odd buttons', '.button-select', async (el, index) => {
+ *   if (index % 2) await el.click();
+ * });
+ * ```
+ *
+ * Check all elements for visibility:
+ *
+ * ```js
+ * // this example works with Playwright and Puppeteer helper
+ * const assert = require('assert');
+ * await eachElement('check all items are visible', '.item', async (el) => {
+ *   assert(await el.isVisible());
+ * });
+ * ```
+ * This method works with WebDriver, Playwright, Puppeteer, Appium helpers.
+ *
+ * Function parameter `el` represents a matched element.
+ * Depending on a helper API of `el` can be different. Refer to API of corresponding browser testing engine for a complete API list:
+ *
+ * * [Playwright ElementHandle](https://playwright.dev/docs/api/class-elementhandle)
+ * * [Puppeteer](https://pptr.dev/#?product=Puppeteer&show=api-class-elementhandle)
+ * * [webdriverio element](https://webdriver.io/docs/api)
+ *
+ * #### Configuration
+ *
+ * * `registerGlobal` - to register `eachElement` function globally, true by default
+ *
+ * If `registerGlobal` is false you can use eachElement from the plugin:
+ *
+ * ```js
+ * const eachElement = codeceptjs.container.plugins('eachElement');
+ * ```
+ *
+ * @param {string}  purpose
+ * @param {CodeceptJS.LocatorOrString}  locator
+ * @param {Function}  fn
+ * @return {Promise<*> | undefined}
+ */
+function eachElement(purpose, locator, fn) {
+  if (store.dryRun) return;
+  const helpers = Object.values(container.helpers());
+
+  const helper = helpers.filter(h => !!h._locate)[0];
+
+  if (!helper) {
+    throw new Error('No helper enabled with _locate method with returns a list of elements.');
+  }
+
+  if (!isAsyncFunction(fn)) {
+    throw new Error('Async function should be passed into each element');
+  }
+
+  const step = new Step(helper, `${purpose || 'each element'} within "${locator}"`);
+  step.helperMethod = '_locate';
+  // eachElement('select all users', 'locator', async (el) => {
+  event.dispatcher.emit(event.step.before, step);
+
+  return recorder.add('register each element wrapper', async () => {
+    event.emit(event.step.started, step);
+    const els = await helper._locate(locator);
+    output.debug(`Found ${els.length} elements for each elements to iterate`);
+
+    const errs = [];
+    let i = 0;
+    for (const el of els) {
+      try {
+        await fn(el, i);
+      } catch (err) {
+        output.error(`eachElement: failed operation on element #${i} ${el}`);
+        errs.push(err);
+      }
+      i++;
+    }
+
+    if (errs.length) {
+      event.dispatcher.emit(event.step.after, step);
+      event.emit(event.step.failed, step, errs[0]);
+      event.emit(event.step.finished, step);
+      throw errs[0];
+    }
+
+    event.dispatcher.emit(event.step.after, step);
+    event.emit(event.step.passed, step, null);
+    event.emit(event.step.finished, step);
+  });
+}
+
+module.exports = function (config) {
+  config = Object.assign(defaultConfig, config);
+
+  if (config.registerGlobal) {
+    global.eachElement = eachElement;
+  }
+  return eachElement;
+}


Test patch:
diff --git a/examples/checkout_test.js b/examples/checkout_test.js
index 3cc4dd92..85dd95b5 100644
--- a/examples/checkout_test.js
+++ b/examples/checkout_test.js
@@ -6,16 +6,15 @@ Before(({ I }) => {
 
 Scenario('It should fill in checkout page', async ({ I }) => {
   I.fillField('#lastName', 'mik');
+  await eachElement('tick all checkboxes', '.custom-checkbox label', async (el) => {
+    await el.click();
+  });
   await retryTo((retryNum) => {
-    // I.usePlaywrightTo('set 0', async ({ browserContext }) => {
-    //   await browserContext.setDefaultTimeout(0);
-    // });
-    console.log('retry', retryNum);
     I.fillField('Promo code', '123345');
     I.click('Redeem');
     I.click(locate('h6').withText('Third item'));
     I.checkOption('#same-address');
-    I.see('Paymen1t');
+    I.see('Payment');
   }, 3);
   I.click('Paypal');
   I.click('Checkout');
diff --git a/test/data/helper.js b/test/data/helper.js
index c426266e..a0b95316 100644
--- a/test/data/helper.js
+++ b/test/data/helper.js
@@ -52,6 +52,10 @@ class MyHelper extends Helper {
   stringWithScenarioType(type) {
     return `Test: I'm ${type} test`;
   }
+
+  _locate() {
+    return [{ name: 'el1' }, { name: 'el2' }];
+  }
 }
 
 module.exports = MyHelper;
diff --git a/test/unit/bdd_test.js b/test/unit/bdd_test.js
index 9a4b738d..d20f7b30 100644
--- a/test/unit/bdd_test.js
+++ b/test/unit/bdd_test.js
@@ -3,6 +3,7 @@ const { Parser } = require('gherkin');
 const {
   Given,
   When,
+  And,
   Then,
   matchStep,
   clearSteps,
@@ -64,10 +65,12 @@ describe('BDD', () => {
   it('should load step definitions', () => {
     Given('I am a bird', () => 1);
     When('I fly over ocean', () => 2);
-    Then(/I see (.*?)/, () => 3);
+    And(/^I fly over land$/i, () => 3);
+    Then(/I see (.*?)/, () => 4);
     expect(1).is.equal(matchStep('I am a bird')());
-    expect(3).is.equal(matchStep('I see ocean')());
-    expect(3).is.equal(matchStep('I see world')());
+    expect(3).is.equal(matchStep('I Fly oVer Land')());
+    expect(4).is.equal(matchStep('I see ocean')());
+    expect(4).is.equal(matchStep('I see world')());
   });
 
   it('should contain tags', async () => {
diff --git a/test/unit/plugin/eachElement_test.js b/test/unit/plugin/eachElement_test.js
new file mode 100644
index 00000000..924d0904
--- /dev/null
+++ b/test/unit/plugin/eachElement_test.js
@@ -0,0 +1,45 @@
+const path = require('path');
+const { expect } = require('chai');
+const container = require('../../../lib/container');
+const eachElement = require('../../../lib/plugin/eachElement')();
+const recorder = require('../../../lib/recorder');
+
+describe('eachElement plugin', () => {
+  beforeEach(() => {
+    global.codecept_dir = path.join(__dirname, '/../..');
+    recorder.start();
+    container.create({
+      helpers: {
+        MyHelper: {
+          require: './data/helper',
+        },
+      },
+    });
+  });
+
+  afterEach(() => {
+    container.clear();
+  });
+
+  it('should iterate for each elements', async () => {
+    let counter = 0;
+    await eachElement('some action', 'some locator', async (el) => {
+      expect(el).is.not.null;
+      counter++;
+    });
+    await recorder.promise();
+    expect(counter).to.equal(2);
+  });
+
+  it('should not allow non async function', async () => {
+    let errorCaught = false;
+    try {
+      await eachElement('some action', 'some locator', (el) => {});
+      await recorder.promise();
+    } catch (err) {
+      errorCaught = true;
+      expect(err.message).to.include('Async');
+    }
+    expect(errorCaught).is.true;
+  });
+});