Repository: codeceptjs/CodeceptJS (js)

Issue:
Playwright I.haveRequestHeaders affects only the first opened tab
#### What are you trying to achieve?
Using Playwright helper and having set additional custom headers with I.haveRequestHeaders,
I want them be effective on all subsequent tabs open from the main tab.

#### What do you get instead?
Custom headers are effective on the first tab. All other ones opened from the main tab miss the custom headers


### Details

```js
codeceptVersion:  "3.1.2"
nodeInfo:  14.15.3
osInfo:  macOS 11.4
cpuInfo:  (8) x64 Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
chromeInfo:  92.0.4515.159
edgeInfo:  Not Found
firefoxInfo:  Not Found
safariInfo:  14.1.1
helpers:  {
 "Playwright": {
  "url": "http://localhost",
  "browser": "chromium",
  "show": true,
  "fullPageScreenshots": true,
  "restart": true,
  "keepBrowserState": false,
  "keepCookies": false,
  "waitForAction": 1000,
  "waitForTimeout": 1000,
  "manualStart": false,
  "waitForNavigation": "load",
  "pressKeyDelay": 1,
  "video": true,
  "keepVideoForPassedTests": true,
  "recordVideo": {},
  "trace": true,
  "keepTraceForPassedTests": false,
 },
 "ChaiWrapper": {
  "require": "codeceptjs-chai"
 },
 "FileSystem": {},
}
plugins:  {
 "screenshotOnFail": {
  "enabled": true
 },
 "wdio": {
  "enabled": false,
  "services": [
   "selenium-standalone"
  ],
  "seleniumArgs": {
   "drivers": {
    "chrome": {
     "version": "93.0.4577.15"
    },
    "firefox": {
     "version": "0.29.0"
    }
   }
  },
  "seleniumInstallArgs": {
   "baseURL": "https://selenium-release.storage.googleapis.com",
   "drivers": {
    "chrome": {
     "version": "93.0.4577.15"
    },
    "firefox": {
     "version": "0.29.0"
    }
   }
  }
 },
 "retryFailedStep": {
  "enabled": true
 },
 "pauseOnFail": {
  "enabled": false
 },
 "stepByStepReport": {
  "enabled": false,
  "screenshotsForAllureReport": true,
  "ignoreSteps": [
   "wait*"
  ]
 },
 "allure": {
  "enabled": true,
  "outputDir": "allureResults"
 },
 "selenoid": {
  "require": "./plugins/selenoid",
  "enabled": false,
  "deletePassed": true,
  "autoCreate": false,
  "autoStart": false,
  "sessionTimeout": "10m",
  "enableVideo": true,
  "enableLog": true,
  "enableVnc": false,
  "videoScreenSize": "1460x920"
 },
 "autoDelay": {
  "enabled": true
 },
 "tryTo": {
  "enabled": true
 },
 "finalResult": {
  "require": "./plugins/final-result",
  "enabled": true
 },
 "logger": {
  "require": "./plugins/logger",
  "enabled": false
 }
}
```

Pull request description:
Playwright I.haveRequestHeaders should affect all the tabs
## Motivation/Description of the PR
- Once custom headers are applied with I.haveRequestHeaders, they will be effective not only on the first tab, but on all ones opened from the first one
- Resolves #3048

Applicable helpers:

- [ ] WebDriver
- [ ] Puppeteer
- [ ] Nightmare
- [ ] REST
- [ ] FileHelper
- [ ] Appium
- [ ] Protractor
- [ ] TestCafe
- [x] Playwright

Applicable plugins:

- [ ] allure
- [ ] autoDelay
- [ ] autoLogin
- [ ] customLocator
- [ ] pauseOnFail
- [ ] coverage
- [ ] retryFailedStep
- [ ] screenshotOnFail
- [ ] selenoid
- [ ] stepByStepReport
- [ ] wdio
- [ ] subtitles

## 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:

- [ ] 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/bdd.md b/docs/bdd.md
index 9c11594c..48a0bcd8 100644
--- a/docs/bdd.md
+++ b/docs/bdd.md
@@ -264,8 +264,10 @@ You can also use the `parse()` method to obtain an object that allow you to get
 - `raw()` - returns the table as a 2-D array
 - `rows()` - returns the table as a 2-D array, without the first row
 - `hashes()` - returns an array of objects where each row is converted to an object (column header is the key)
+- `rowsHash()` - returns an object where each row corresponds to an entry(first column is the key, second column is the value)
+- `transpose()` - transpose the data, returns nothing. To work with the transposed table use the methods above.
 
-If we use hashes() with the previous exemple :
+If we use hashes() with the previous example :
 
 ```js
 Given('I have products in my cart', (table) => { // eslint-disable-line
@@ -281,7 +283,59 @@ Given('I have products in my cart', (table) => { // eslint-disable-line
   }
 });
 ```
+Examples of tables using: 
 
+```gherkin
+  Given I have a short employees card
+    | Harry | Potter  |
+    | Chuck | Norris  |
+```
+```js
+const { DataTableArgument } = require('codeceptjs');
+//...
+Given('I have a short employees card', (table) => {
+  const dataTableArgument = new DataTableArgument(table);
+  const raw = dataTableArgument.raw(); 
+  // row = [['Harry', 'Potter'], ['Chuck', 'Norris']]
+  dataTableArgument.transpose();
+  const transposedRaw = dataTableArgument.raw();
+  // transposedRaw = [['Harry', 'Chuck'], ['Potter', 'Norris']];
+  }
+);
+```
+```gherkin
+  Given I have an employee card
+    | name  | surname | position |
+    | Harry | Potter  | Seeker   |
+```
+```js
+const { DataTableArgument } = require('codeceptjs');
+//...
+Given('I have an employee card', (table) => {
+  const dataTableArgument = new DataTableArgument(table);
+  const hashes = dataTableArgument.hashes(); 
+  // hashes = [{ name: 'Harry', surname: 'Potter', position: 'Seeker' }];
+  const rows = dataTableArgument.rows();
+  // rows = [['Harry', 'Potter', Seeker]];
+  }
+);
+```
+```gherkin
+  Given I have a formatted employee card
+    | name     | Harry  |
+    | surname  | Potter |
+    | position | Seeker |
+```
+```js
+const { DataTableArgument } = require('codeceptjs');
+//...
+Given('I have a formatted employee card', (table) => {
+  const dataTableArgument = new DataTableArgument(table);
+  const rawHash = dataTableArgument.rowsHash();
+  // rawHash = { name: 'Harry', surname: 'Potter', position: 'Seeker' };
+  }
+);
+```
 ### Examples
 
 In case scenarios represent the same logic but differ on data, we can use *Scenario Outline* to provide different examples for the same behavior. Scenario outline is just like a basic scenario with some values replaced with placeholders, which are filled from a table. Each set of values is executed as a different test.
diff --git a/docs/helpers/Appium.md b/docs/helpers/Appium.md
index b1c09682..b38c8f3d 100644
--- a/docs/helpers/Appium.md
+++ b/docs/helpers/Appium.md
@@ -1027,6 +1027,19 @@ let email = await I.grabValueFrom('input[name=email]');
 
 Returns **[Promise][13]&lt;[string][4]>** attribute value
 
+### saveScreenshot
+
+Saves a screenshot to ouput folder (set in codecept.json or codecept.conf.js).
+Filename is relative to output folder.
+
+```js
+I.saveScreenshot('debug.png');
+```
+
+#### Parameters
+
+-   `fileName` **[string][4]** file name to save.
+
 ### scrollIntoView
 
 Scroll element into viewport.
@@ -1663,25 +1676,11 @@ I.saveElementScreenshot(`#submit`,'debug.png');
 -   `locator` **([string][4] \| [object][6])** element located by CSS|XPath|strict locator.
 -   `fileName` **[string][4]** file name to save.
 
-### saveScreenshot
-
-Saves a screenshot to ouput folder (set in codecept.json or codecept.conf.js).
-Filename is relative to output folder.
-
-```js
-I.saveScreenshot('debug.png');
-```
-
-#### Parameters
-
--   `fileName` **[string][4]** file name to save.
--   `fullPage` **[boolean][20]** (optional, `false` by default) flag to enable fullscreen screenshot mode. (optional, default `false`)
-
 ### type
 
 Types out the given text into an active field.
 To slow down typing use a second parameter, to set interval between key presses.
-_Note:_ Should be used when [`fillField`][21] is not an option.
+_Note:_ Should be used when [`fillField`][20] is not an option.
 
 ```js
 // passing in a string
@@ -1919,6 +1918,4 @@ Returns **([Promise][13]&lt;DOMRect> | [Promise][13]&lt;[number][8]>)** Element
 
 [19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
 
-[20]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
-
-[21]: #fillfield
+[20]: #fillfield
diff --git a/docs/helpers/Puppeteer.md b/docs/helpers/Puppeteer.md
index 84a2b5da..38cb5b67 100644
--- a/docs/helpers/Puppeteer.md
+++ b/docs/helpers/Puppeteer.md
@@ -101,6 +101,8 @@ This helper should be configured in codecept.json or codecept.conf.js
 }
 ```
 
+> Note: When connecting to remote browser `show` and specific `chrome` options (e.g. `headless` or `devtools`) are ignored.
+
 #### Example #5: Target URL with provided basic authentication
 
 ```js
@@ -115,7 +117,21 @@ This helper should be configured in codecept.json or codecept.conf.js
 }
 ```
 
-Note: When connecting to remote browser `show` and specific `chrome` options (e.g. `headless` or `devtools`) are ignored.
+#### Troubleshooting
+
+Error Message:  `No usable sandbox!`
+
+When running Puppeteer on CI try to disable sandbox if you see that message
+
+    helpers: {
+     Puppeteer: {
+        url: 'http://localhost',
+        show: false,
+        chrome: {
+          args: ['--no-sandbox', '--disable-setuid-sandbox']
+        }
+      },
+    }
 
 ## Access From Helpers
 
diff --git a/docs/plugins.md b/docs/plugins.md
index a47a9c7b..592aedca 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -417,7 +417,7 @@ Dumps code coverage from Playwright/Puppeteer after every test.
 
 ```js
 plugins: {
-   playwrightCoverage: {
+   coverage: {
      enabled: true
    }
 }
diff --git a/lib/config.js b/lib/config.js
index 60980be4..73e8f857 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -128,7 +128,7 @@ function loadConfigFile(configFile) {
   const extensionName = path.extname(configFile);
 
   // .conf.js config file
-  if (extensionName === '.js' || extensionName === '.ts') {
+  if (extensionName === '.js' || extensionName === '.ts' || extensionName === '.cjs') {
     return Config.create(require(configFile).config);
   }
 
diff --git a/lib/data/dataTableArgument.js b/lib/data/dataTableArgument.js
index 4df02843..b3295348 100644
--- a/lib/data/dataTableArgument.js
+++ b/lib/data/dataTableArgument.js
@@ -1,4 +1,9 @@
+/**
+ * DataTableArgument class to store the Cucumber data table from
+ * a step as an object with methods that can be used to access the data.
+ */
 class DataTableArgument {
+  /** @param {*} gherkinDataTable */
   constructor(gherkinDataTable) {
     this.rawData = gherkinDataTable.rows.map((row) => {
       return row.cells.map((cell) => {
@@ -7,16 +12,25 @@ class DataTableArgument {
     });
   }
 
+  /** Returns the table as a 2-D array
+  * @returns {string[][]}
+  */
   raw() {
     return this.rawData.slice(0);
   }
 
+  /** Returns the table as a 2-D array, without the first row
+  * @returns {string[][]}
+  */
   rows() {
     const copy = this.raw();
     copy.shift();
     return copy;
   }
 
+  /** Returns an array of objects where each row is converted to an object (column header is the key)
+  * @returns {any[]}
+  */
   hashes() {
     const copy = this.raw();
     const header = copy.shift();
@@ -26,6 +40,27 @@ class DataTableArgument {
       return r;
     });
   }
+
+  /** Returns an object where each row corresponds to an entry
+   * (first column is the key, second column is the value)
+  * @returns {Record<string, string>}
+  */
+  rowsHash() {
+    const rows = this.raw();
+    const everyRowHasTwoColumns = rows.every((row) => row.length === 2);
+    if (!everyRowHasTwoColumns) {
+      throw new Error('rowsHash can only be called on a data table where all rows have exactly two columns');
+    }
+    /** @type {Record<string, string>} */
+    const result = {};
+    rows.forEach((x) => (result[x[0]] = x[1]));
+    return result;
+  }
+
+  /** Transposed the data */
+  transpose() {
+    this.rawData = this.rawData[0].map((x, i) => this.rawData.map((y) => y[i]));
+  }
 }
 
 module.exports = DataTableArgument;
diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js
index 8ff7cedd..175b9ab6 100644
--- a/lib/helper/Playwright.js
+++ b/lib/helper/Playwright.js
@@ -780,7 +780,7 @@ class Playwright extends Helper {
     if (!customHeaders) {
       throw new Error('Cannot send empty headers.');
     }
-    return this.page.setExtraHTTPHeaders(customHeaders);
+    return this.browserContext.setExtraHTTPHeaders(customHeaders);
   }
 
   /**
diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index bc9eea24..df76346b 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -481,7 +481,7 @@ class WebDriver extends Helper {
     try {
       require('webdriverio');
     } catch (e) {
-      return ['webdriverio@^5.2.2'];
+      return ['webdriverio@^6.12.1'];
     }
   }
 
diff --git a/lib/index.js b/lib/index.js
index f93eb180..96332500 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -32,6 +32,8 @@ module.exports = {
   within: require('./within'),
   /**  @type {typeof CodeceptJS.DataTable} */
   dataTable: require('./data/table'),
+  /**  @type {typeof CodeceptJS.DataTableArgument} */
+  dataTableArgument: require('./data/dataTableArgument'),
   /** @type {typeof CodeceptJS.store} */
   store: require('./store'),
   /** @type {typeof CodeceptJS.Locator} */
diff --git a/lib/interfaces/gherkin.js b/lib/interfaces/gherkin.js
index febf88b9..e1839ed3 100644
--- a/lib/interfaces/gherkin.js
+++ b/lib/interfaces/gherkin.js
@@ -11,9 +11,10 @@ const transform = require('../transform');
 const parser = new Parser();
 parser.stopAtFirstError = false;
 
-module.exports = (text) => {
+module.exports = (text, file) => {
   const ast = parser.parse(text);
 
+  if (!ast.feature) {throw new Error(`No 'Features' available in Gherkin '${file}' provided!`);}
   const suite = new Suite(ast.feature.name, new Context());
   const tags = ast.feature.tags.map(t => t.name);
   suite.title = `${suite.title} ${tags.join(' ')}`.trim();
diff --git a/lib/mochaFactory.js b/lib/mochaFactory.js
index 96f822ed..affe5691 100644
--- a/lib/mochaFactory.js
+++ b/lib/mochaFactory.js
@@ -35,8 +35,7 @@ class MochaFactory {
       if (mocha.suite.suites.length === 0) {
         mocha.files
           .filter(file => file.match(/\.feature$/))
-          .map(file => fs.readFileSync(file, 'utf8'))
-          .forEach(content => mocha.suite.addSuite(gherkinParser(content)));
+          .forEach(file => mocha.suite.addSuite(gherkinParser(fs.readFileSync(file, 'utf8'), file)));
 
         // remove feature files
         mocha.files = mocha.files.filter(file => !file.match(/\.feature$/));
diff --git a/lib/plugin/coverage.js b/lib/plugin/coverage.js
index fa74099e..e6126a51 100644
--- a/lib/plugin/coverage.js
+++ b/lib/plugin/coverage.js
@@ -48,7 +48,7 @@ function buildFileName(test, uniqueFileName) {
  *
  * ```js
  * plugins: {
- *    playwrightCoverage: {
+ *    coverage: {
  *      enabled: true
  *    }
  * }
diff --git a/typings/index.d.ts b/typings/index.d.ts
index fdcf6e48..1b99668d 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -111,6 +111,7 @@ declare const pause: typeof CodeceptJS.pause;
 declare const within: typeof CodeceptJS.within;
 declare const session: typeof CodeceptJS.session;
 declare const DataTable: typeof CodeceptJS.DataTable;
+declare const DataTableArgument: typeof CodeceptJS.DataTableArgument;
 declare const codeceptjs: typeof CodeceptJS.index;
 declare const locate: typeof CodeceptJS.Locator.build;
 declare function inject(): CodeceptJS.SupportObject;
@@ -160,6 +161,7 @@ declare namespace NodeJS {
     within: typeof within;
     session: typeof session;
     DataTable: typeof DataTable;
+    DataTableArgument: typeof DataTableArgument;
     locate: typeof locate;
     inject: typeof inject;
     secret: typeof secret;
diff --git a/typings/jsdoc.conf.js b/typings/jsdoc.conf.js
index f451551e..9459201e 100644
--- a/typings/jsdoc.conf.js
+++ b/typings/jsdoc.conf.js
@@ -7,6 +7,7 @@ module.exports = {
       './lib/config.js',
       './lib/container.js',
       './lib/data/table.js',
+      './lib/data/dataTableArgument.js',
       './lib/event.js',
       './lib/helper/clientscripts/nightmare.js',
       './lib/index.js',


Test patch:
diff --git a/test/unit/data/dataTableArgument_test.js b/test/unit/data/dataTableArgument_test.js
index ce8ff397..2e8c81ec 100644
--- a/test/unit/data/dataTableArgument_test.js
+++ b/test/unit/data/dataTableArgument_test.js
@@ -1,4 +1,5 @@
 const { expect } = require('chai');
+const { it } = require('mocha');
 const DataTableArgument = require('../../../lib/data/dataTableArgument');
 
 describe('DataTableArgument', () => {
@@ -44,6 +45,27 @@ describe('DataTableArgument', () => {
     ],
   };
 
+  const gherkinDataTableWithColumnHeader = {
+    rows: [
+      {
+        type: 'TableRow',
+        location: { line: 59, column: 13 },
+        cells: [
+          { type: 'TableCell', location: [Object], value: 'firstName' },
+          { type: 'TableCell', location: [Object], value: 'Chuck' },
+        ],
+      },
+      {
+        type: 'TableRow',
+        location: { line: 59, column: 13 },
+        cells: [
+          { type: 'TableCell', location: [Object], value: 'lastName' },
+          { type: 'TableCell', location: [Object], value: 'Norris' },
+        ],
+      },
+    ],
+  };
+
   it('should return a 2D array containing each row', () => {
     const dta = new DataTableArgument(gherkinDataTable);
     const raw = dta.raw();
@@ -64,4 +86,19 @@ describe('DataTableArgument', () => {
     const expectedRows = [{ firstName: 'Chuck', lastName: 'Norris' }];
     expect(rows).to.deep.equal(expectedRows);
   });
+
+  it('transpose should transpose the gherkin data table', () => {
+    const dta = new DataTableArgument(gherkinDataTable);
+    dta.transpose();
+    const raw = dta.raw();
+    const expectedRaw = [['John', 'Chuck'], ['Doe', 'Norris']];
+    expect(raw).to.deep.equal(expectedRaw);
+  });
+
+  it('rowsHash returns an object where the keys are the first column', () => {
+    const dta = new DataTableArgument(gherkinDataTableWithColumnHeader);
+    const rawHash = dta.rowsHash();
+    const expectedRaw = { firstName: 'Chuck', lastName: 'Norris' };
+    expect(rawHash).to.deep.equal(expectedRaw);
+  });
 });