Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cucumber-js + Playwright + Angular 13 modules - ES Module loading problem #1987

Closed
testgitdl opened this issue Apr 5, 2022 · 11 comments
Closed
Labels
❓ question Consider using support forums: https://cucumber.io/tools/cucumber-open/support

Comments

@testgitdl
Copy link

testgitdl commented Apr 5, 2022

👓 What did you see?

I have setup a Angular 13 project that uses both cucumber-js and playwright. I use cucumber-js as the test runner. This project uses ES Modules as I have "type":"module" in my package.json.
When I try to run my tests with cucumber-js I get ES Module errors

✅ What did you expect to see?

Should be able to run my cucumber tests

📦 Which tool/library version are you using?

Tried also cucumber version 7.3.0 and 8.0.0-rc3. below my package.json

{
"name": "cucumber-playwright",
"version": "1.0.0",
"type": "module",
"scripts": {
"video": "PWVIDEO=1 cucumber-js",
"format": "prettier --write \"/*.{ts,tsx,css,html}\" ",
"only": "npm run cucumber -- --tags @only",
"steps-usage": "cucumber-js features//.feature --dry-run",
"all": "cucumber-js features/**/.feature",
"test-chrome": "cucumber-js",
"test-firefox": "SET BROWSER=firefox && cucumber-js",
"test:parallel": "cucumber-js --parallel=2",
"test-generate-report": "node src/support/reportHTML.js && node src/support/reportJUNIT.js"
},
"dependencies": {
"@cucumber/cucumber": "8.0.0-rc.3",
"@cucumber/html-formatter": "18.0.0",
"@cucumber/pretty-formatter": "1.0.0-alpha.2",
"@types/fs-extra": "9.0.13",
"cucumber-console-formatter": "1.0.0",
"cucumber-html-reporter": "5.5.0",
"expect": "27.5.1",
"playwright": "1.20.1"
},
"devDependencies": {
"@types/chai": "^4.3.0",
"@types/expect": "24.3.0",
"@types/lodash": "4.14.180",
"@types/node": "16.11.26",
"@typescript-eslint/eslint-plugin": "5.16.0",
"@typescript-eslint/parser": "5.16.0",
"allure-commandline": "^2.17.2",
"chai": "^4.3.6",
"cucumber-html-reporter": "^5.5.0",
"cucumber-junit-convert": "^2.1.1",
"cucumber-junit-formatter": "^0.2.2",
"cucumberjs-junitxml": "^1.0.0",
"e2e-api-playwright": "file://D:/cucumber-playwright-master/e2e-api-playwright-1.0.0.tgz",
"eslint": "8.12.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-prettier": "4.0.0",
"fs-extra": "10.0.1",
"open": "8.4.0",
"prettier": "2.6.1",
"rimraf": "3.0.2",
"standard-version": "9.3.2",
"ts-node": "^10.7.0",
"typescript": "4.6.3"
}

🔬 How could we reproduce it?

This is the way my project looks like
features
->playwright.feature
node_modules
reports
screenshots
src
->steps
---->playwright.steps.ts
->support
---->common-hooks.ts
---->config-chrome.ts
---->config-firefox.ts
---->config.ts
---->custom-world.ts
cucumber.js
tsconfig.json
package.json
e2e-api-playwright-1.0.0.tgz

tsconfig.json:

{
"compilerOptions": {
"baseUrl": "./",
"target": "es2020",
"module": "commonjs",
"sourceMap": true,
"outDir": "./build",
"noEmit": true,
"strict": true,
"noImplicitAny": false,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipDefaultLibCheck": true,
"resolveJsonModule": true,
"lib": [
"dom",
"es2020"
],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
"exclude": [
"node_modules"
]
}

cucumber.js:

const common = `
  --require-module ts-node/register
  --require-module tsconfig-paths/register
  --require src/**/*.ts
  --require support/config.ts
  --format json:reports/report.json 
  --format html:reports/cucumber-html-report.html
  --format summary 
  --format progress-bar 
  --format @cucumber/pretty-formatter
  --format-options ${JSON.stringify({ snippetInterface: 'async-await' })}
  --publish-quiet
  `;

const getWorldParams = () => {
  const params = {
    foo: 'bar',
  };

  return `--world-parameters ${JSON.stringify({ params })}`;
};

module.exports = {
  default: `${common} ${getWorldParams()}`,
};

playwright.feature:

@foo
Feature: Playwright tests

Background:
Given Go to the playwright website

@runME
Scenario: Failing test
Then I get an error

Scenario: Passing test
Then I get a passing test

playwright.steps.ts:

import { ICustomWorld } from '../support/custom-world';
import { config } from '../support/config';
import { Given, Then } from '@cucumber/cucumber';
import expect from 'expect';
import { Placeholder } from 'e2e-api-playwright';
let myPO: Placeholder;

myPO = new Placeholder('Emma');

Given('Go to the playwright website', async function (this: ICustomWorld) {
const page = this.page!;
await page.goto(config.BASE_URL);
await page.locator('nav >> a >> text="Playwright"').waitFor();
await myPO.setText();
});

Then('I get an error', async function () {
expect(true).toEqual(false);
})

Then('I get a passing test', async function () {
expect(true).toEqual(true);
})

Steps to reproduce the behavior:

  1. Do an npm install (this installs also the page object library - e2e-api-playwright)
  2. Try to run the tests via npm run test-chrome
  3. See error 'Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: D:\cucumber-playwright-master\src\steps\playwright.steps.ts
    require() of ES modules is not supported.
    require() of D:\cucumber-playwright-master\src\steps\playwright.steps.ts from D:\cucumber-playwright-master\node_modules@cucumber\cucumber\lib\api\support.js is an ES module file as it is a .ts file whose nearest parent package.json contains "type": "module" which defines all .ts files in that package scope as ES modules.
    Instead change the requiring code to use import(), or remove "type": "module" from D:\cucumber-playwright-master\package.json.'
  4. If I remove from package.json the "type": "module" then I get error importing the PO. The error is: Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: D:\cucumber-playwright-master\node_modules\e2e-api-playwright\fesm2015\e2e-api-playwright.mjs

📚 Any additional context?

Details related to the e2e-api-playwright library:
e2e-api-playwright
->src
---->lib
-------->placeholder.ts
---->public_api.ts
ng-package.json
package.json
tsconfig.lib.json

placeholder.ts:
export class Placeholder {
public t: string;
constructor(private el: string) {
this.t = this.el;
}

public setText() {
console.log('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' + this.t);
}
}

public_api.ts:
export { Placeholder } from './lib/placeholder';

tsconfig.lib.json:
{
"compilerOptions": {
"outDir": "../out-tsc/lib",
"baseUrl": ".",
"declarationMap": true,
"target": "es2020",
"module": "es2020",
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"inlineSources": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"types": [],
"lib": [
"dom",
"es2020"
]
},
"angularCompilerOptions": {
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}

@aurelien-reeves
Copy link
Contributor

Did you properly configured typescript to emit CJS code?
As explained in the documentation, when using a transpiler like typescript, ESM is not supported yet.

Do you think you could set-up a minimal reproducible example within a public repo?
That would be far easier for us to investigate

@davidjgoss
Copy link
Contributor

davidjgoss commented Apr 5, 2022

I think your Cucumber setup looks mostly fine, except this script:

"all": "cucumber-js features/**/.feature",

I don't think that will run anything, and you mean:

"all": "cucumber-js features/**/*.feature",

Although that's where we look by default anyway so you could just omit the argument entirely.


I think your problem is with trying to mix CommonJS and ESM in your project.

Your main project is CommonJS, per your TypeScript config, so it will compile down to require() statements. If you set your package.json type field to "module" this breaks it for the reasons it says, so you shouldn't do that.

That leaves you with the error loading code from e2e-api-playwright, which is because your compiled TypeScript code is trying to require() a module that is ESM. I think your best course short term is to make that library (looks like an internal one to your company) publish as CommonJS for now.

In theory you could dynamic import your library e.g.

const { Placeholder } = await import('e2e-api-playwright')

This doesn't play easily with TypeScript though.

@davidjgoss davidjgoss added the ❓ question Consider using support forums: https://cucumber.io/tools/cucumber-open/support label Apr 5, 2022
@testgitdl
Copy link
Author

Thank you @davidjgoss and @aurelien-reeves for the support. I will check further and let you know.
I've uploaded my project here: https://github.com/testgitdl/cucumber-playwright-ang13

@testgitdl
Copy link
Author

It is working :) The weird esm error came from "cucumber-junit-formatter": "^0.2.2".
The steps and page objects are not TypeScript but JavaScript - saw that there is an open issue on that and will wait to have it fixed.
The remaining problem that I have is that in the steps.js and login.page.js I do not have any intellisense in VS Code. If I type this.page I would like to see all the Playwright methods that are available. I've tried to create a CustomWorld.js but without success.
@davidjgoss and @aurelien-reeves hope you can help

CustomWorld.js looks like this:
import { World } from '@cucumber/cucumber';
export class CustomWorld extends World {
page = null;
context = null;
constructor(options) {
super(options);
}
async init(scenario) {
this.context = await global.context;
this.page = await browser.page;
}
}

hooks.js looks like this:
import { Before, BeforeAll, AfterAll, After, setDefaultTimeout, Status, setWorldConstructor } from "@cucumber/cucumber";
import { chromium, firefox } from "playwright";
import { CustomWorld } from "./CustomWorld.js"

setDefaultTimeout(60000)
setWorldConstructor(CustomWorld);

const configChrome = {
slowMo: 0,
headless: false,
channel: "chrome",
args: [
"--no-sandbox",
"--no-zygote",
"--start-fullscreen"
]
};

const configFirefox = {
slowMo: 0,
args: ['--use-fake-ui-for-media-stream', '--use-fake-device-for-media-stream'],
headless: false,
firefoxUserPrefs: {
'media.navigator.streams.fake': true,
'media.navigator.permission.disabled': true,
}
}

// launch the browser
BeforeAll(async function () {
switch (process.env.BROWSER?.trim()) {
case 'firefox':
global.browser = await firefox.launch(configFirefox);
break;
case 'chrome':
global.browser = await chromium.launch(configChrome);
break;
default:
global.browser = await chromium.launch(configChrome);
}
});

// close the browser
AfterAll(async function () {
await global.browser.close();
});

// Create a new browser context and page per scenario
Before(async function (scenario) {
global.context = await global.browser.newContext();
global.page = await global.context.newPage();
// this.content = await global.browser.newContext();
// this.page = await global.context.newPage();
this.init(scenario);
});

// Cleanup after each scenario
After(async function () {
await global.page.close();
await global.context.close();
// await this.page.close();
// await this.content.close();
});

After(async function (scenario) {
if (scenario.result.status === Status.FAILED) {
var buffer = await global.page.screenshot({ path: screenshots/${scenario.pickle.name}.png, fullPage: true })
this.attach(buffer, 'image/png');
}
});

cucumber.js looks like this:
export default {
import: ['e2e/support/.js', 'e2e/step_definitions/.js'],
paths: ['e2e/features/*.feature'],
format: [
'json:reports/report.json',
'html:reports/cucumber-html-report.html',
'summary',
'progress - bar',
'@cucumber/pretty-formatter'
],
publishQuiet: true
}

I've pushed the changes in the https://github.com/testgitdl/cucumber-playwright-ang13

@davidjgoss
Copy link
Contributor

davidjgoss commented Apr 10, 2022

@testgitdl glad you got it working!

I had a quick look at your project (thanks for posting), because you're setting some stuff globally it's difficult, but you might be able to add a TS declaration file that augments the global scope - whether IntelliSense would pick this up I don't know.

Alternatively:

  • Per esm: add support for loaders #1844 (comment) you should be able to try using ts-node/esm with your TypeScript project and cucumber-js 8.0.0 now (see the diff I linked on one of our example projects).
  • Consider not sharing a global browser instance and instead creating a new browser for each scenario (i.e. each instance of your world which you can do async in a Before hook). You'd avoid the global state and also have a stronger separation of scenarios in terms of state and side effects.

With that I'll close this issue since there are no ESM-related issues in play any more 👍

@testgitdl
Copy link
Author

testgitdl commented Apr 21, 2022

Hello @davidjgoss! Many thanks for the support!!!
With v 8.0.0 I got my project to work - everything was fine. Now I just upgraded to cucumber version 8.1.0 and it seems I'm no longer able to run my tests. I get this error the below error when running "node --loader ts-node/esm ./node_modules/@cucumber/cucumber/bin/cucumber-js"

D:\Test\cucumber-playwright-master\node_modules\ts-node\dist-raw\node-esm-resolve-implementation.js:383
throw new ERR_MODULE_NOT_FOUND(
^
CustomError: Cannot find module 'D:\Test\cucumber-playwright-master\node_modules@cucumber\cucumber\bin\cucumber-js' imported from D:\Test\cucumber-playwright-master
at finalizeResolution (D:\Test\cucumber-playwright-master\node_modules\ts-node\dist-raw\node-esm-resolve-implementation.js:383:11)
at moduleResolve (D:\Test\cucumber-playwright-master\node_modules\ts-node\dist-raw\node-esm-resolve-implementation.js:818:10)
at Object.defaultResolve (D:\Test\cucumber-playwright-master\node_modules\ts-node\dist-raw\node-esm-resolve-implementation.js:929:11)
at D:\Test\cucumber-playwright-master\node_modules\ts-node\src\esm.ts:228:33
at entrypointFallback (D:\Test\cucumber-playwright-master\node_modules\ts-node\src\esm.ts:179:34)
at resolve (D:\Test\cucumber-playwright-master\node_modules\ts-node\src\esm.ts:227:12)
at ESMLoader.resolve (node:internal/modules/esm/loader:530:30)
at ESMLoader.getModuleJob (node:internal/modules/esm/loader:251:18)
at ESMLoader.import (node:internal/modules/esm/loader:332:22)
at node:internal/modules/run_main:54:28

The working project is avail here: https://github.com/testgitdl/cucumber-playwright-ang13

Also one question, not related but maybe you can help... I am not able to make use of the paths defined in my tsconfig.json. So for example in the login.page.ts
I'm able to import my page as:
import { page } from '../support/hooks.js';
but not as:
import { page } from '@support';
although the path is defined in the tsconfig.json

@davidjgoss
Copy link
Contributor

davidjgoss commented Apr 21, 2022

@testgitdl

Your issue with 8.1.1 is that the binary file itself was renamed to fix issues with most ESM loaders. You should avoid pointing at the file itself and instead use the alias that npm creates:

node --loader ts-node/esm ./node_modules/.bin/cucumber-js

That should do it.

Also one question, not related but maybe you can help

Not sure on that one I'm afraid - sorry!

@testgitdl
Copy link
Author

@davidjgoss unfortunately then it is another error. Thx for the quick reply!!!

D:\Test\Playwright_Tally\cucumber-playwright-master\node_modules.bin\cucumber-js:2
basedir=$(dirname "$(echo "$0" | sed -e 's,\,/,g')")
^^^^^^^

SyntaxError: missing ) after argument list
at Object.compileFunction (node:vm:352:18)
at wrapSafe (node:internal/modules/cjs/loader:1032:15)
at Module._compile (node:internal/modules/cjs/loader:1067:27)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1157:10)
at Module.load (node:internal/modules/cjs/loader:981:32)
at Function.Module._load (node:internal/modules/cjs/loader:822:12)
at ModuleWrap. (node:internal/modules/esm/translators:168:29)
at ModuleJob.run (node:internal/modules/esm/module_job:197:25)
at async Promise.all (index 0)
at async ESMLoader.import (node:internal/modules/esm/loader:337:24)

@testgitdl
Copy link
Author

@davidjgoss what also is strange is that in my imports I add the .js extension and if I try to remove it like stated in this post (https://stackoverflow.com/questions/63807613/running-node-with-loader-ts-node-esm-js-requires-imports-to-have-the-js-extensi) then I get an invalid module error.
In the page.po.ts the import from hooks.ts only works with .js extension: import { page } from '../support/hooks';
If I remove it does not work and if I add the experimental-specifier-resolution (node --experimental-specifier-resolution=node --loader ts-node/esm ./node_modules/@cucumber/cucumber/bin/cucumber-js) then I get an invalid module error:
TypeError [ERR_INVALID_MODULE_SPECIFIER]: Invalid module "file:///D:/Test/cucumber-playwright-master/node_modules/@cucumber/cucumber/bin/cucumber-js"
at new NodeError (node:internal/errors:371:5)
at ESMLoader.load (node:internal/modules/esm/loader:380:13)
at async ESMLoader.moduleProvider (node:internal/modules/esm/loader:280:47)
at async link (node:internal/modules/esm/module_job:70:21) {

@davidjgoss
Copy link
Contributor

v8.1.2 has been released which re-adds the original cucumber-js file under bin to cover cases where it is referenced directly.

@testgitdl
Copy link
Author

@davidjgoss thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
❓ question Consider using support forums: https://cucumber.io/tools/cucumber-open/support
Projects
None yet
Development

No branches or pull requests

3 participants