I am currently trying to use expect to do assertions by using const { expect } = require('#playwright/test'); but every time I get Error: Cannot find module '#playwright/test'. It is a very short script but something is wrong with that.
const { chromium } = require("playwright");
const { expect } = require('#playwright/test');
const { matchers } = require('playwright-expect');
console.log("##########", expect)
// add custom matchers
expect.extend(matchers);
(async () => {
const browser = await chromium.launch({
headless: false,
});
const page = await browser.newPage();
await page.goto("someurl");
await page.fill("input[name='userLoginId']", 'nnn');
await page.fill("input[name='password']", 'nnn');
await page.click("button[type=submit]");
})();
package.json
{
"name": "playwright",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "node ./index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"playwright": "^1.15.1",
"playwright-expect": "^0.1.2"
}
}
The test works fine without this:
const { expect } = require('#playwright/test');
const { matchers } = require('playwright-expect');
console.log("##########", expect)
// add custom matchers
expect.extend(matchers);
And it does what I ask it to do, but now that I want to do assertions and I add that, now it does not work.
You have to install #playwright/test library:
npm i -D #playwright/test
Do not use playwright-expect library. Playwright already includes web-first assertions. Hence, there is no reason to use an additional library to extend expect.
Remove unused code:
const { matchers } = require('playwright-expect');
console.log("##########", expect)
// add custom matchers
expect.extend(matchers);
I've created an issue about the same question here https://github.com/microsoft/playwright/issues/14971 and I'll update the result when it's answered.
While trying to use playwright as a library i did run into the same error
Error: Cannot find module '#playwright/test'
To fix this error in my case the following needed to be done. The pre-condition is that playwright is already installed.
Create a node js project
Create a folder: mkdir HelloLibrary
Inside this folder create a file from the command prompt: echo var msg = 'Hello World'; console.log(msg); > app.js
Open a command prompt and run node app.js
Add playwright npm i -D playwright
Change content of app.js like code sample below
Run it again node app.js
Voila done
App.js sample
This code launches a browser and takes a screenshot
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 50 });
const page = await browser.newPage();
await page.goto('http://whatsmyuseragent.org/');
await page.screenshot({ path: `example.png` });
await browser.close();
})();
See also
Tutorial: Node.js for Beginners
Use playwright as a library
Related
I am trying to make test case suite with mocha and mongodb-memory-server(as an in memory db). I am trying to implement this in the way below.
Project structure:
test runner: (in package.json)
"scripts": {
"test": "mocha 'app/**/*.spec.js' --recursive --exit"
},
So, First I need to initialise the in memory MongoDB, thats why I am using global.spec.js which looks like this,
const { MongoMemoryServer } = require("mongodb-memory-server");
const mongoose = require("mongoose");
mongoose.set("usePushEach", true);
let mongoServer;
before(async function () {
// mongod donwload on first time
this.timeout(30 * 1000);
mongoServer = new MongoMemoryServer();
const mongoUri = await mongoServer.getUri();
await mongoose.connect(mongoUri, {});
process.env.AGENDA_DB_URI = mongoUri;
});
after(function () {
mongoose.disconnect();
mongoServer.stop();
});
and a test-setup.js file which looks like this,
const { MongoMemoryServer } = require("mongodb-memory-server");
(async function() {
// trigger downloading mongodb executable on first time
const mongoServer = new MongoMemoryServer();
await mongoServer.getUri();
mongoServer.stop();
})()
.then(() => {
process.exit(0);
})
.catch(err => {
console.error(err);
process.exit(1);
});
all *.spec.js files will be inside modules folder. In simple word each folder inside module will have one .spec.js file. If I try to run this using the npm run test command it is throwing me some error, that looks like this,
1) "before all" hook
0 passing (448ms)
1 failing
1) "before all" hook:
Uncaught Error: TypeError: logWarnFn is not a function
I believe this logWarnFn error is coming from i18n. But when i start the server it is working fine.
versions:
"mongodb-memory-server": "^6.6.2",
"mocha": "^6.0.2",
"i18n": "0.8.3",
I'm using Puppeteer to test a client function within a react environment - the function itself doesn't use React, but is meant to be imported in es6 react modules and run inside a end user DOM environment. I need Puppeteer since this function relies on properties such as innerText, that aren't available in jsdom.
This function takes a DOM element as an argument, however I am having trouble writing test files for it. Here is a sample of my code:
import path from 'path';
import puppeteer from 'puppeteer';
import {getSelectionRange, setSelectionRange} from './selection';
describe(
'getSelection should match setSelection',
() => {
let browser;
let page;
beforeAll(async done => {
try {
browser = await puppeteer.launch();
page = await browser.newPage();
await page.goto(
`file://${path.join(process.env.ROOT,
'testFiles/selection_range_test.html')}`
);
await page.exposeFunction(
'setSelectionRange',
(el, start, end) => setSelectionRange(el, start, end)
);
await page.exposeFunction(
'getSelectionRange',
el => getSelectionRange(el)
);
} catch(error) {
console.error(error);
}
done();
});
afterAll(async done => {
await browser.close();
done();
});
it('should match on a node with only one text node children', async () => {
const {selection, element, argEl} = await page.evaluate(async () => {
const stn = document.getElementById('single-text-node');
// Since console.log will output in the Puppeteer browser and not in node console,
// I added a line inside the selectionRange function to return the element it receives
// as an argument.
const argEl = await window.setSelectionRange(stn, 1, 10);
const selectionRange = await window.getSelectionRange(stn);
return {selection: selectionRange, element: stn, argEl};
});
// Outputs <div id="single-text-node">...</div>
// (the content is long so I skipped it, but it displays the correct value here)
console.log(element.outerHTML);
// Outputs {}
console.log(argEl);
});
}
);
As described in the comments, the element that is directly returned from page.evaluate() is correct, but when passed as an argument, the function receives an empty object. I suspect a scope issue but I am totally out of solutions here.
Sadly I couldn't find any solution that wouldn't invoke transpiling my files, but hopefully I managed to make it work correctly.
The key point was to create a second transpile configuration that will generate a code directly usable by a web browser, using UMD format. Since I use rollup, here is my rollup,config.js file:
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs';
import resolve from 'rollup-plugin-node-resolve';
import pkg from './package.json';
// The content that is actually exported to be used within a React or Node application.
const libConfig = [
{
inlineDynamicImports: true,
input: './src/index.js',
output: [
{
file: './lib/index.js',
format: 'cjs'
},
],
external: [...Object.keys(pkg.dependencies || {})],
plugins: [
commonjs(),
resolve(),
babel({exclude: 'node_modules/**'})
]
}
];
// Used to generate a bundle that is directly executable within a browser environment, for E2E testing.
const testConfig = [
{
inlineDynamicImports: true,
input: './src/index.js',
output: [
{
file: './dist/index.js',
format: 'umd',
name: 'tachyon'
},
],
external: [...Object.keys(pkg.dependencies || {})],
plugins: [
commonjs(),
resolve(),
babel({runtimeHelpers: true})
]
}
];
const config = process.env.NODE_ENV === 'test' ? testConfig : libConfig;
export default config;
I then rewrote my scripts a bit so my test bundle is generated on each test run.
package.json
{
"scripts": {
"build:test": "NODE_ENV=test rollup -c && NODE_ENV=",
"build": "rollup -c",
"test": "yarn build:test && jest"
},
}
Finally, I added the transpiled script to my selection_test.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Selection range test</title>
<script src="../dist/index.js"></script>
</head>
...
Which lets me write my test file like this:
import path from 'path';
import puppeteer from 'puppeteer';
import {describe, beforeAll, afterAll, it} from '#jest/globals';
describe(
'getSelection should match setSelection',
() => {
let browser;
let page;
beforeAll(async done => {
try {
browser = await puppeteer.launch({
headless: true,
args: ['--disable-web-security', '--disable-features=SameSiteByDefaultCookies,CookiesWithoutSameSiteMustBeSecure'],
});
page = await browser.newPage();
await page.goto(`file://${path.join(process.env.ROOT, 'tests/selection_test.html')}`, {waitUntil: 'networkidle0'});
await page.setBypassCSP(true);
} catch(error) {
console.error(error);
}
done();
});
afterAll(async done => {
await browser.close();
done();
});
it('should match on a node with only one text node children', async () => {
const data = await page.evaluate(() => {
// Fix eslint warnings.
window.tachyon = window.tachyon || null;
if (window.tachyon == null) {
return new Error(`cannot find tachyon module`);
}
const stn = document.getElementById('single-text-node');
const witnessRange = tachyon.setRange(stn, 1, 10);
const selectionRange = tachyon.getRange(stn);
return {witnessRange, selectionRange, element: stn.outerHTML};
});
console.log(data); // Outputs the correct values
/*
{
witnessRange: { start: 1, end: 10 },
selectionRange: {
absolute: { start: 1, end: 10 },
start: { container: {}, offset: 1 },
end: { container: {}, offset: 10 }
},
element: '<div id="single-text-node">Lorem ... sem.</div>'
}
*/
});
}
);
The only remaining issue is that start.container and end.container within the results of getRange are undefined, but it seems more likely an issue from puppeteer that cannot handle the Range startContainer and endContainer properties - I was able to pass DOM references between the content of page.evaluate and my module function without any issues, so it doesn't look like the problem anymore.
const { getIo } = require('services/socketio');
const restful = require('utils/restful');
const publicApiService = require('services/publicApi');
const accessTokenMiddleware = require('middleware/accessToken');
const validateSessionCookieMiddleware = require('middleware/validateSessionCookie');
const logger = require('utils/logger');
package.json:
"scripts": {
"start": "node ./bin/www",
...
}
I get this error:
internal/modules/cjs/loader.js:796
throw err;
^
Error: Cannot find module 'services/socketio'
I tried running set NODE_PATH=./lib in terminal but it doesn't fix it.
Your local modules should start with ./ - so change it to './services/publicApi'.
I got this code from another Stackoverflow Question:
import electron from "electron";
import puppeteer from "puppeteer-core";
const delay = (ms: number) =>
new Promise(resolve => {
setTimeout(() => {
resolve();
}, ms);
});
(async () => {
try {
const app = await puppeteer.launch({
executablePath: electron,
args: ["."],
headless: false,
});
const pages = await app.pages();
const [page] = pages;
await page.setViewport({ width: 1200, height: 700 });
await delay(5000);
const image = await page.screenshot();
console.log(image);
await page.close();
await delay(2000);
await app.close();
} catch (error) {
console.error(error);
}
})();
Typescript compiler complains about executablePath property of launch method options object cause it needs to be of type string and not Electron. So how to pass electron chromium executable path to puppeteer?
You cannot use electron executable with Puppeteer directly without some workarounds and flag changes. They have tons of differences in the API. Specially electron doesn't have all of the chrome.* API which is needed for chromium browser to work properly, many flags still doesn't have proper replacements such as the headless flag.
Below you will see two ways to do it. However you need to make sure of two points,
Make sure the puppeteer is connected before the app is initiated.
Make sure you get the correct version puppeteer or puppeteer-core for the version of Chrome that is running in Electron!
Use puppeteer-in-electron
There are lots of workarounds, but most recently there is a puppeteer-in-electron package which allows you to run puppeteer within electron app using the electron.
First, install the dependencies,
npm install puppeteer-in-electron puppeteer-core electron
Then run it.
import {BrowserWindow, app} from "electron";
import pie from "puppeteer-in-electron";
import puppeteer from "puppeteer-core";
const main = async () => {
const browser = await pie.connect(app, puppeteer);
const window = new BrowserWindow();
const url = "https://example.com/";
await window.loadURL(url);
const page = await pie.getPage(browser, window);
console.log(page.url());
window.destroy();
};
main();
Get the debugging port and connect to it
The another way is to get the remote-debugging-port of the electron app and connect to it. This solution is shared by trusktr on electron forum.
import {app, BrowserWindow, ...} from "electron"
import fetch from 'node-fetch'
import * as puppeteer from 'puppeteer'
app.commandLine.appendSwitch('remote-debugging-port', '8315')
async function test() {
const response = await fetch(`http://localhost:8315/json/versions/list?t=${Math.random()}`)
const debugEndpoints = await response.json()
let webSocketDebuggerUrl = debugEndpoints['webSocketDebuggerUrl ']
const browser = await puppeteer.connect({
browserWSEndpoint: webSocketDebuggerUrl
})
// use puppeteer APIs now!
}
// ... make your window, etc, the usual, and then: ...
// wait for the window to open/load, then connect Puppeteer to it:
mainWindow.webContents.on("did-finish-load", () => {
test()
})
Both solution above uses webSocketDebuggerUrl to resolve the issue.
Extra
Adding this note because most people uses electron to bundle the app.
If you want to build the puppeteer-core and puppeteer-in-electron, you need to use hazardous and electron-builder to make sure get-port-cli works.
Add hazardous on top of main.js
// main.js
require ('hazardous');
Make sure the get-port-cli script is unpacked, add the following on package.json
"build": {
"asarUnpack": "node_modules/get-port-cli"
}
Result after building:
the toppest answer dones't work for me use electron 11 and puppeteer-core 8.
but start puppeteer in main process other then in the renderer process works for me.you can use ipcMain and ipcRenderer to comunicate each other.the code below
main.ts(main process code)
import { app, BrowserWindow, ipcMain } from 'electron';
import puppeteer from 'puppeteer-core';
async function newGrabBrowser({ url }) {
const browser = await puppeteer.launch({
headless: false,
executablePath:
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
});
const page = await browser.newPage();
page.goto(url);
}
ipcMain.on('grab', (event, props) => {
newGrabBrowser(JSON.parse(props));
});
home.ts (renderer process code)
const { ipcRenderer } = require('electron');
ipcRenderer.send('grab',JSON.stringify({url: 'https://www.google.com'}));
There is also another option, which works for electron 5.x.y and up (currently up to 7.x.y, I did not test it on 8.x.y beta yet):
// const assert = require("assert");
const electron = require("electron");
const kill = require("tree-kill");
const puppeteer = require("puppeteer-core");
const { spawn } = require("child_process");
let pid;
const run = async () => {
const port = 9200; // Debugging port
const startTime = Date.now();
const timeout = 20000; // Timeout in miliseconds
let app;
// Start Electron with custom debugging port
pid = spawn(electron, [".", `--remote-debugging-port=${port}`], {
shell: true
}).pid;
// Wait for Puppeteer to connect
while (!app) {
try {
app = await puppeteer.connect({
browserURL: `http://localhost:${port}`,
defaultViewport: { width: 1000, height: 600 } // Optional I think
});
} catch (error) {
if (Date.now() > startTime + timeout) {
throw error;
}
}
}
// Do something, e.g.:
// const [page] = await app.pages();
// await page.waitForSelector("#someid")//
// const text = await page.$eval("#someid", element => element.innerText);
// assert(text === "Your expected text");
// await page.close();
};
run()
.then(() => {
// Do something
})
.catch(error => {
// Do something
kill(pid, () => {
process.exit(1);
});
});
Getting the pid and using kill is optional. For running the script on some CI platform it does not matter, but for local environment you would have to close the electron app manually after each failed try.
Please see this sample repo.
I am trying to do BDD with cucumber-js and drive the browser testing with Headless Chrome and puppeteer.
Using the documentation from cucumber node example and headless chrome, I get the following errors, the entire code base is avaliable here: github repo.
Errors:
TypeError: this.browser.newPage is not a function
TypeError: this.browser.close is not a function
// features/support/world.js
const puppeteer = require('puppeteer');
var {defineSupportCode} = require('cucumber');
function CustomWorld() {
this.browser = puppeteer.launch();
}
defineSupportCode(function({setWorldConstructor}) {
setWorldConstructor(CustomWorld)
})
// features/step_definitions/hooks.js
const puppeteer = require('puppeteer');
var {defineSupportCode} = require('cucumber');
defineSupportCode(function({After}) {
After(function() {
return this.browser.close();
});
});
// features/step_definitions/browser_steps.js
const puppeteer = require('puppeteer');
var { defineSupportCode } = require('cucumber');
defineSupportCode(function ({ Given, When, Then }) {
Given('I am on the Cucumber.js GitHub repository', function (callback) {
const page = this.browser.newPage();
return page.goto('https://github.com/cucumber/cucumber-js/tree/master');
});
When('I click on {string}', function (string, callback) {
// Write code here that turns the phrase above into concrete actions
callback(null, 'pending');
});
Then('I should see {string}', function (string, callback) {
// Write code here that turns the phrase above into concrete actions
callback(null, 'pending');
});
});
puppeteer is completely async, so you have to wait it's initialization before using this.browser.
But setWorldConstructor is sync function, so you can't wait there. In my example I used Before hook
My example:
https://gist.github.com/dmitrika/7dee618842c00fbc35418b901735656b
We created puppeteer-cucumber-js to simplify working with Puppeteer and Cucumber:
Run npm install puppeteer-cucumber-js
Create a features folder in the root of your project
Add a feature-name.feature file with your Given, When, Then statements
Create a features/step-definitions folder
Add JavaScript steps to execute for each of your features steps
Run tests node ./node_modules/puppeteer-cucumber-js/index.js --headless
Source code with a working example on GitHub
Cucumber has been since updated. This is how I have implemented my async puppeteer setup with cucumber. Gist here
const { BeforeAll, Before, AfterAll, After } = require('cucumber');
const puppeteer = require('puppeteer');
Before(async function() {
const browser = await puppeteer.launch({ headless: false, slowMo: 50 });
const page = await browser.newPage();
this.browser = browser;
this.page = page;
})
After(async function() {
// Teardown browser
if (this.browser) {
await this.browser.close();
}
// Cleanup DB
})