Jest-puppeteer: window/document not defined in import

Created on 29 Apr 2019  ·  6Comments  ·  Source: smooth-code/jest-puppeteer

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

question ❓

Most helpful comment

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 ?

All 6 comments

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

Was this page helpful?
0 / 5 - 0 ratings