Repository: codeceptjs/CodeceptJS (js)

Issue:
Support for playwright-core
#### Proposal
Need support for `playwright-core` for projects where browser binaries are managed separately and don't need the full weight of installing `playwright`. 

https://github.com/microsoft/playwright/tree/master/packages/playwright-core

Pull request:
feat(#2663): support for playwright-core usage
## Motivation/Description of the PR
- Resolves #2663 .

Added a simple utility that allows the require fallback of npm packages. If the first is not found it will try the next one in the chain.
I have been using it to include playwright-core instead of playwright because I don't want to download the browser binaries on yarn/npm install. This approach can be used for other helpers as well if you guys think it's worth it.

For example, it would be possible as well for puppeteer without adding 

```
npm config set puppeteer_skip_chromium_download true
```

as explained in https://github.com/codeceptjs/CodeceptJS/issues/2076
 

Applicable helpers:

- [ ] WebDriver
- [ ] Puppeteer
- [ ] Nightmare
- [ ] REST
- [ ] FileHelper
- [ ] Appium
- [ ] Protractor
- [ ] TestCafe
- [x] Playwright

Applicable plugins:

- [ ] allure
- [ ] autoDelay
- [ ] autoLogin
- [ ] customLocator
- [ ] pauseOnFail
- [ ] coverage
- [ ] retryFailedStep
- [ ] screenshotOnFail
- [ ] selenoid
- [ ] stepByStepReport
- [ ] stepTimeout
- [ ] wdio
- [ ] subtitles

## Type of change

- [ ] :fire: Breaking changes
- [x] :rocket: New functionality
- [ ] :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/helpers/Playwright.md b/docs/helpers/Playwright.md
index 3dfde1a9..15a1522a 100644
--- a/docs/helpers/Playwright.md
+++ b/docs/helpers/Playwright.md
@@ -1695,11 +1695,11 @@ Use Playwright API inside a test.
 First argument is a description of an action.
 Second argument is async function that gets this helper as parameter.
 
-{ [`page`][23], [`context`][24] [`browser`][25] } objects from Playwright API are available.
+{ [`page`][23], [`browserContext`][24] [`browser`][25] } objects from Playwright API are available.
 
 ```js
-I.usePlaywrightTo('emulate offline mode', async ({ context }) => {
-  await context.setOffline(true);
+I.usePlaywrightTo('emulate offline mode', async ({ browserContext }) => {
+  await browserContext.setOffline(true);
 });
 ```
 
diff --git a/docs/translation.md b/docs/translation.md
index 59b1eef5..9fc00983 100644
--- a/docs/translation.md
+++ b/docs/translation.md
@@ -57,7 +57,7 @@ To write your tests in portuguese you can enable the portuguese translation in c
 Now you can write test like this:
 
 ```js
-Scenario('Efetuar login', ({ Eu }) => {
+Cenário('Efetuar login', ({ Eu }) => {
     Eu.estouNaPagina('http://minhaAplicacao.com.br');
     Eu.preenchoOCampo("login", "usuario@minhaAplicacao.com.br");
     Eu.preenchoOCampo("senha", "123456");
diff --git a/lib/helper/ApiDataFactory.js b/lib/helper/ApiDataFactory.js
index 0e34048a..526e5034 100644
--- a/lib/helper/ApiDataFactory.js
+++ b/lib/helper/ApiDataFactory.js
@@ -257,13 +257,16 @@ class ApiDataFactory extends Helper {
    * // create user with defined email
    * // and receive it when inside async function
    * const user = await I.have('user', { email: 'user@user.com'});
+   * // create a user with options that will not be included in the final request
+   * I.have('user', { }, { age: 33, height: 55 })
    * ```
    *
    * @param {*} factory factory to use
    * @param {*} params predefined parameters
+   * @param {*} options options for programmatically generate the attributes
    */
-  have(factory, params) {
-    const item = this._createItem(factory, params);
+  have(factory, params, options) {
+    const item = this._createItem(factory, params, options);
     this.debug(`Creating ${factory} ${JSON.stringify(item)}`);
     return this._requestCreate(factory, item);
   }
@@ -277,21 +280,25 @@ class ApiDataFactory extends Helper {
    *
    * // create 3 posts by one author
    * I.haveMultiple('post', 3, { author: 'davert' });
+   * 
+   * // create 3 posts by one author with options
+   * I.haveMultiple('post', 3, { author: 'davert' }, { publish_date: '01.01.1997' });
    * ```
    *
    * @param {*} factory
    * @param {*} times
    * @param {*} params
+   * @param {*} options
    */
-  haveMultiple(factory, times, params) {
+  haveMultiple(factory, times, params, options) {
     const promises = [];
     for (let i = 0; i < times; i++) {
-      promises.push(this.have(factory, params));
+      promises.push(this.have(factory, params, options));
     }
     return Promise.all(promises);
   }
 
-  _createItem(model, data) {
+  _createItem(model, data, options) {
     if (!this.factories[model]) {
       throw new Error(`Factory ${model} is not defined in config`);
     }
@@ -303,7 +310,7 @@ class ApiDataFactory extends Helper {
         modulePath = path.join(global.codecept_dir, modulePath);
       }
       const builder = require(modulePath);
-      return builder.build(data);
+      return builder.build(data, options);
     } catch (err) {
       throw new Error(`Couldn't load factory file from ${modulePath}, check that
 
diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js
index 49df07ed..f417263b 100644
--- a/lib/helper/Playwright.js
+++ b/lib/helper/Playwright.js
@@ -19,6 +19,7 @@ const {
   getNormalizedKeyAttributeValue,
   isModifierKey,
   clearString,
+  requireWithFallback,
 } = require('../utils');
 const {
   isColorProperty,
@@ -49,11 +50,17 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
  *
  * This helper works with a browser out of the box with no additional tools required to install.
  *
- * Requires `playwright` package version ^1 to be installed:
+ * Requires `playwright` or `playwright-core` package version ^1 to be installed:
  *
  * ```
  * npm i playwright@^1 --save
  * ```
+ * or
+ * ```
+ * npm i playwright-core@^1 --save
+ * ```
+ *
+ * Using playwright-core package, will prevent the download of browser binaries and allow connecting to an existing browser installation or for connecting to a remote one.
  *
  * ## Configuration
  *
@@ -235,7 +242,7 @@ class Playwright extends Helper {
   constructor(config) {
     super(config);
 
-    playwright = require('playwright');
+    playwright = requireWithFallback('playwright', 'playwright-core');
 
     // set defaults
     this.isRemoteBrowser = false;
@@ -334,7 +341,7 @@ class Playwright extends Helper {
 
   static _checkRequirements() {
     try {
-      require('playwright');
+      requireWithFallback('playwright', 'playwright-core');
     } catch (e) {
       return ['playwright@^1'];
     }
@@ -506,11 +513,11 @@ class Playwright extends Helper {
   * First argument is a description of an action.
   * Second argument is async function that gets this helper as parameter.
   *
-  * { [`page`](https://github.com/microsoft/playwright/blob/main/docs/src/api/class-page.md), [`context`](https://github.com/microsoft/playwright/blob/main/docs/src/api/class-browsercontext.md) [`browser`](https://github.com/microsoft/playwright/blob/main/docs/src/api/class-browser.md) } objects from Playwright API are available.
+  * { [`page`](https://github.com/microsoft/playwright/blob/main/docs/src/api/class-page.md), [`browserContext`](https://github.com/microsoft/playwright/blob/main/docs/src/api/class-browsercontext.md) [`browser`](https://github.com/microsoft/playwright/blob/main/docs/src/api/class-browser.md) } objects from Playwright API are available.
   *
   * ```js
-  * I.usePlaywrightTo('emulate offline mode', async ({ context }) => {
-  *   await context.setOffline(true);
+  * I.usePlaywrightTo('emulate offline mode', async ({ browserContext }) => {
+  *   await browserContext.setOffline(true);
   * });
   * ```
   *
diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js
index fa5e21e6..f818d51f 100644
--- a/lib/helper/Puppeteer.js
+++ b/lib/helper/Puppeteer.js
@@ -22,6 +22,7 @@ const {
   screenshotOutputFolder,
   getNormalizedKeyAttributeValue,
   isModifierKey,
+  requireWithFallback,
 } = require('../utils');
 const {
   isColorProperty,
@@ -43,7 +44,15 @@ const consoleLogStore = new Console();
  * Browser control is executed via DevTools Protocol (instead of Selenium).
  * This helper works with a browser out of the box with no additional tools required to install.
  *
- * Requires `puppeteer` package to be installed.
+ * Requires `puppeteer` or `puppeteer-core` package to be installed.
+ * ```
+ * npm i puppeteer --save
+ * ```
+ * or
+ * ```
+ * npm i puppeteer-core --save
+ * ```
+ * Using `puppeteer-core` package, will prevent the download of browser binaries and allow connecting to an existing browser installation or for connecting to a remote one.
  *
  * > Experimental Firefox support [can be activated](https://codecept.io/helpers/Puppeteer-firefox).
  *
@@ -182,7 +191,7 @@ class Puppeteer extends Helper {
   constructor(config) {
     super(config);
 
-    puppeteer = require('puppeteer');
+    puppeteer = requireWithFallback('puppeteer', 'puppeteer-core');
 
     // set defaults
     this.isRemoteBrowser = false;
@@ -245,7 +254,7 @@ class Puppeteer extends Helper {
 
   static _checkRequirements() {
     try {
-      require('puppeteer');
+      requireWithFallback('puppeteer', 'puppeteer-core');
     } catch (e) {
       return ['puppeteer'];
     }
diff --git a/lib/utils.js b/lib/utils.js
index de20060e..75ea0c0a 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -431,3 +431,23 @@ module.exports.modifierKeys = modifierKeys;
 module.exports.isModifierKey = function (key) {
   return modifierKeys.includes(key);
 };
+
+module.exports.requireWithFallback = function (...packages) {
+  const exists = function (pkg) {
+    try {
+      require.resolve(pkg);
+    } catch (e) {
+      return false;
+    }
+
+    return true;
+  };
+
+  for (const pkg of packages) {
+    if (exists(pkg)) {
+      return require(pkg);
+    }
+  }
+
+  throw new Error(`Cannot find modules ${packages.join(',')}`);
+};
diff --git a/translations/pt-BR.js b/translations/pt-BR.js
index 8bff406f..befd770d 100644
--- a/translations/pt-BR.js
+++ b/translations/pt-BR.js
@@ -1,5 +1,13 @@
 module.exports = {
   I: 'Eu',
+  contexts: {
+    Feature: 'Funcionalidade',
+    Scenario: 'Cenário',
+    Before: 'Antes',
+    After: 'Depois',
+    BeforeSuite: 'AntesDaSuite',
+    AfterSuite: 'DepoisDaSuite',
+  },
   actions: {
     amOutsideAngularApp: 'naoEstouEmAplicacaoAngular',
     amInsideAngularApp: 'estouNaAplicacaoAngular',


Test patch:
diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js
index 6865fcaf..cdefdabb 100644
--- a/test/helper/Playwright_test.js
+++ b/test/helper/Playwright_test.js
@@ -653,6 +653,16 @@ describe('Playwright', function () {
       });
       assert.equal('TestEd Beta 2.0', title);
     });
+
+    it('should pass expected parameters', async () => {
+      await I.amOnPage('/');
+      const params = await I.usePlaywrightTo('test', async (params) => {
+        return params;
+      });
+      expect(params.page).to.exist;
+      expect(params.browserContext).to.exist;
+      expect(params.browser).to.exist;
+    });
   });
 
   describe('#mockRoute, #stopMockingRoute', () => {
diff --git a/test/unit/container_test.js b/test/unit/container_test.js
index f78659fd..76b38f15 100644
--- a/test/unit/container_test.js
+++ b/test/unit/container_test.js
@@ -53,6 +53,14 @@ describe('Container', () => {
       expect(container.translation().I).to.eql('Je');
       expect(container.translation().value('contexts').Feature).to.eql('Fonctionnalité');
     });
+
+    it('should create Portuguese translation', () => {
+      container.create({ translation: 'pt-BR' });
+      expect(container.translation()).to.be.instanceOf(Translation);
+      expect(container.translation().loaded).to.be.true;
+      expect(container.translation().I).to.eql('Eu');
+      expect(container.translation().value('contexts').Feature).to.eql('Funcionalidade');
+    });
   });
 
   describe('#helpers', () => {
diff --git a/test/unit/utils_test.js b/test/unit/utils_test.js
index 47a55f5e..90c08480 100644
--- a/test/unit/utils_test.js
+++ b/test/unit/utils_test.js
@@ -334,4 +334,15 @@ describe('utils', () => {
       }
     });
   });
+
+  describe('#requireWithFallback', () => {
+    it('returns the fallback package', () => {
+      expect(utils.requireWithFallback('unexisting-package', 'playwright')).eql(require('playwright'));
+    });
+
+    it('returns provide default require not found message', () => {
+      expect(() => utils.requireWithFallback('unexisting-package', 'unexisting-package2'))
+        .to.throw(Error, 'Cannot find modules unexisting-package,unexisting-package2');
+    });
+  });
 });