Hi,
I set up a jest-puppeteer project following the README. In my test file I import a library which works on browser DOM (document.querySelector() ...) but I get the following error when running tests:
ReferenceError: document is not defined
So I tried to switch to jsdom by adding the following comments at the top of the file:
/**
* @jest-environment jsdom
*/
But I then get the error:
ReferenceError: page is not defined
How can I do to import my library and still having browser/page puppeteer variables set ?
regards
I get it work by habving PuppeteerEnvironment extending JsdomEnvironment instead of NodeEnvironment
// const NodeEnvironment = require('jest-environment-node');
const JsdomEnvironment = require('jest-environment-jsdom');
const fs = require('fs');
const path = require('path');
const puppeteer = require('puppeteer');
const os = require('os');
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');
class PuppeteerEnvironment extends JsdomEnvironment {
constructor(config) {
super(config);
}
async setup() {
await super.setup();
// get the wsEndpoint
const wsEndpoint = fs.readFileSync(path.join(DIR, 'wsEndpoint'), 'utf8');
if (!wsEndpoint) {
throw new Error('wsEndpoint not found');
}
// connect to puppeteer
this.global.__BROWSER__ = await puppeteer.connect({
browserWSEndpoint: wsEndpoint,
});
}
async teardown() {
await super.teardown();
}
runScript(script) {
return super.runScript(script);
}
}
module.exports = PuppeteerEnvironment;
it is the right way to do it ?
hi @CreativeBulma
I have some question, why you should work on browser DOM?
Instead NodeEnvironment to JsdomEnvironment is not a good way,.
I have to test a library which manipulate the DOM.
And I'd like to test the API of this library directly within my puppeteer test file (some test are directly on the library API and some others are on the DOM in browser). But if I use NodeEnvironment my library won't load due to document not defined error.
For example: I'd like to instanciate my library on DOM using:
const instance = await page.evaluate(() => {
new myLibrary('.selector');
});
and check instance variable is typeof myLibrary:
expect(typeof instance).toBe('object');
expect(instance._class == 'myLibrary');
So I'll be able to test my library is able to find the correct DOM node and instanciate itself.
Here is my customized version of @CreativeBulma's solution (thank you):
.jest-environment-puppeteer-jsdom.js:
// eslint-disable-next-line
// import NodeEnvironment from 'jest-environment-node'
const JsdomEnvironment = require('jest-environment-jsdom');
// import chalk from 'chalk'
const readConfig = require('./.read-config');
const handleError = error => {
process.emit('uncaughtException', error)
}
const KEYS = {
CONTROL_C: '\u0003',
CONTROL_D: '\u0004',
ENTER: '\r',
}
class PuppeteerEnvironment extends JsdomEnvironment {
// Jest is not available here, so we have to reverse engineer
// the setTimeout function, see https://github.com/facebook/jest/blob/v23.1.0/packages/jest-runtime/src/index.js#L823
setTimeout(timeout) {
if (this.global.jasmine) {
// eslint-disable-next-line no-underscore-dangle
this.global.jasmine.DEFAULT_TIMEOUT_INTERVAL = timeout
} else {
this.global[Symbol.for('TEST_TIMEOUT_SYMBOL')] = timeout
}
}
async setup() {
const config = await readConfig.readConfig()
const puppeteer = readConfig.getPuppeteer(config)
this.global.puppeteerConfig = config
const wsEndpoint = process.env.PUPPETEER_WS_ENDPOINT
if (!wsEndpoint) {
throw new Error('wsEndpoint not found')
}
this.global.browser = await puppeteer.connect({
...config.connect,
...config.launch,
browserURL: undefined,
browserWSEndpoint: wsEndpoint,
})
if (config.browserContext === 'incognito') {
// Using this, pages will be created in a pristine context.
this.global.context = await this.global.browser.createIncognitoBrowserContext()
} else if (config.browserContext === 'default' || !config.browserContext) {
/**
* Since this is a new browser, browserContexts() will return only one instance
* https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#browserbrowsercontexts
*/
this.global.context = await this.global.browser.browserContexts()[0]
} else {
throw new Error(
`browserContext should be either 'incognito' or 'default'. Received '${
config.browserContext
}'`,
)
}
this.global.jestPuppeteer = {
debug: async () => {
// eslint-disable-next-line no-eval
// Set timeout to 4 days
this.setTimeout(345600000)
// Run a debugger (in case Puppeteer has been launched with `{ devtools: true }`)
await this.global.page.evaluate(() => {
// eslint-disable-next-line no-debugger
debugger
})
// eslint-disable-next-line no-console
// console.log(
// chalk.blue('\n\n🕵️ Code is paused, press enter to resume'),
// )
console.log('Code is paused, press enter to resume');
// Run an infinite promise
return new Promise(resolve => {
const { stdin } = process
const onKeyPress = key => {
if (
key === KEYS.CONTROL_C ||
key === KEYS.CONTROL_D ||
key === KEYS.ENTER
) {
stdin.removeListener('data', onKeyPress)
if (!listening) {
stdin.setRawMode(false)
stdin.pause()
}
resolve()
}
}
const listening = stdin.listenerCount('data') > 0
if (!listening) {
stdin.setRawMode(true)
stdin.resume()
stdin.setEncoding('utf8')
}
stdin.on('data', onKeyPress)
})
},
resetPage: async () => {
if (this.global.page) {
this.global.page.removeListener('pageerror', handleError)
await this.global.page.close()
}
this.global.page = await this.global.context.newPage()
if (config && config.exitOnPageError) {
this.global.page.addListener('pageerror', handleError)
}
},
}
await this.global.jestPuppeteer.resetPage()
}
async teardown() {
const { page, context, browser, puppeteerConfig } = this.global
if (page) {
page.removeListener('pageerror', handleError)
}
if (puppeteerConfig && puppeteerConfig.browserContext === 'incognito') {
if (context) {
await context.close()
}
} else if (page) {
await page.close()
}
if (browser) {
await browser.disconnect()
}
}
}
module.exports = PuppeteerEnvironment
.read-config.js:
const fs = require('fs');
const path = require('path');
const promisify = require('util').promisify;
const cwd = require('cwd');
const merge = require('merge-deep');
const exists = promisify(fs.exists)
const DEFAULT_CONFIG = {
launch: {},
browser: 'chromium',
browserContext: 'default',
exitOnPageError: true,
}
const DEFAULT_CONFIG_CI = merge(DEFAULT_CONFIG, {
launch: {
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
],
},
})
exports.readConfig = async function readConfig() {
const defaultConfig =
process.env.CI === 'true' ? DEFAULT_CONFIG_CI : DEFAULT_CONFIG
const hasCustomConfigPath = !!process.env.JEST_PUPPETEER_CONFIG
const configPath =
process.env.JEST_PUPPETEER_CONFIG || 'jest-puppeteer.config.js'
const absConfigPath = path.resolve(cwd(), configPath)
const configExists = await exists(absConfigPath)
if (hasCustomConfigPath && !configExists) {
throw new Error(
`Error: Can't find a root directory while resolving a config file path.\nProvided path to resolve: ${configPath}`,
)
}
if (!hasCustomConfigPath && !configExists) {
return defaultConfig
}
// eslint-disable-next-line global-require, import/no-dynamic-require
const localConfig = await require(absConfigPath)
return merge({}, defaultConfig, localConfig)
}
exports.getPuppeteer = function getPuppeteer(config) {
switch (config.browser.toLowerCase()) {
case 'chromium':
// eslint-disable-next-line global-require, import/no-dynamic-require, import/no-extraneous-dependencies
return require('puppeteer')
case 'firefox':
// eslint-disable-next-line global-require, import/no-dynamic-require, import/no-extraneous-dependencies
return require('puppeteer-firefox')
default:
throw new Error(
`Error: "browser" config option is given an unsupported value: ${browser}`,
)
}
}
jest.config.js:
module.exports = {
"testEnvironment": __dirname + "/.jest-environment-puppeteer-jsdom.js",
...
};
Hi @JumpLink,
can you please explain the difference between your code and mine ?
I'd like to understand what your code is doing in addition to mine ?
regards
Most helpful comment
I get it work by habving PuppeteerEnvironment extending JsdomEnvironment instead of NodeEnvironment
it is the right way to do it ?