Puppeteer is a powerful automation library for Google Chrome. With Puppeteer, you can launch a Chrome browser that you have full control over from Node.js. This makes UI testing easy: your client-side app runs in a real browser, no need to worry about the painful quirks of Jest attempting to mimic a browser in Node.js. Puppeteer runs in headless mode by default, which means the Chrome window isn't visible. But you can still take screenshots in headless mode, or you can disable headless mode and watch your tests click through your app.

In this article, I'll demonstrate how to use Puppeteer to test a simple Vue app using the Mocha test framework.

Getting Started With Puppeteer

First, let's take a look at how Puppeteer works in isolation by writing a Node.js script that searches for "Vue" on google.com .

const puppeteer = require ( 'puppeteer' ); run().then(() => console .log( 'Done' )).catch(error => console .log(error)); async function run ( ) { const browser = await puppeteer.launch({ headless: false , slowMo: 250 }); const page = await browser.newPage(); await page.goto( 'https://google.com' ); await page.evaluate(() => { document .querySelector( 'input[name="q"]' ).value = 'Vue' ; }); const waitForLoad = new Promise (resolve => page.on( 'load' , () => resolve())); await page.evaluate(() => { document .querySelector( 'input[value="Google Search"]' ).click(); }); await waitForLoad; const firstLink = await page.evaluate(() => document .querySelector( '#search link' ).getAttribute( 'href' )); console .log(firstLink); await browser.close(); }

Here's what the script looks like while it is in progress.

The page.evaluate() function is the function you'll use most often with Puppeteer. The evaluate() function lets you execute a function in Chrome, which means you can use standard vanilla JS operations like the querySelector() function to manipulate the page. The function parameter, known as the pageFunction , can also return a result, like the firstLink result above.

Note that the pageFunction runs in the browser, which means it does not have access to any variables in Node.js. For example, the below page.evaluate() will error out because foo is not defined in the browser. Internally, Puppeteer converts pageFunction to a string using Function#toString() before sending it to Chrome.

const foo = 42 ; const res = await page.evaluate(() => foo);

Puppeteer, Express, and Mocha

Node.js' event loop makes testing easy, because you can start an Express server and then launch a Chrome browser with Puppeteer that connects to your Express server, all without worrying about threads. You can even start Webpack to recompile your client-side code in Mocha.

Below is an example of a Mocha test that starts an Express app, and launches a Puppeteer instance that interacts with the Express app.

const assert = require ( 'assert' ); const express = require ( 'express' ); const puppeteer = require ( 'puppeteer' ); describe( 'my app' , function ( ) { let browser; let page; let server; before( async function ( ) { this .timeout( 10000 ); const app = express(); app.get( '*' , (req, res) => res.send( 'Hello, World' )); server = await app.listen( 3000 ); browser = await puppeteer.launch({ headless: false , slowMo: 500 }); page = await browser.newPage(); await page.goto( 'http://localhost:3000' ); }); after( async function ( ) { await browser.close(); await server.close(); }); it( 'works' , async function ( ) { const content = await page.evaluate(() => document .body.innerHTML); assert.equal(content, 'Hello, World' ); }); });

Here's what the controlled Chrome browser looks like while the test is running:

Setting Up a Basic Vue App

Below is a minimal single page app using Vue Router. This app has two routes, one that displays "Home" and one that displays "About."

<html> <body> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> <div id="content"></div> <script type="text/javascript"> const router = new VueRouter({ routes: [ { path: '/home', component: { template: '<h1>Home</h1>' } }, { path: '/about', component: { template: '<h1>About Us</h1>' } } ] }); new Vue({ el: '#content', router, template: ` <div> <div> <router-link to="/home">Home</router-link> <router-link to="/about">About Us</router-link> </div> <div id="route"> <router-view></router-view> </div> </div> ` }); </script> </body> </html>

Here's how you would test that clicking on "Home" displays "Home" and clicking on "About" displays "About Us" using Mocha and Puppeteer:

const assert = require ( 'assert' ); const puppeteer = require ( 'puppeteer' ); describe( 'my app' , function ( ) { let browser; let page; let server; before( async function ( ) { this .timeout( 10000 ); const app = require ( 'express' )(); app.use( require ( 'express-static' )( '.' )); server = await app.listen( 3000 ); browser = await puppeteer.launch({ headless: false , slowMo: 250 }); page = await browser.newPage(); await page.goto( 'http://localhost:3000/index.html' ); }); it( 'displays the current page' , async function ( ) { this .timeout( 10000 ); await page.evaluate(() => document .querySelector( 'a[href="#/home"]' ).click()); let content = await page.evaluate(() => document .querySelector( '#route' ).innerHTML); assert.equal(content, '<h1>Home</h1>' ); await page.evaluate(() => document .querySelector( 'a[href="#/about"]' ).click()); content = await page.evaluate(() => document .querySelector( '#route' ).innerHTML); assert.equal(content, '<h1>About Us</h1>' ); }); });

Moving On

There's a lot of different testing tools out there. Jest lets you run tests directly in Node.js, without the actual browser. Cypress provides you an integrated testing framework that supports multiple browsers. Mocha and Puppeteer is a good middle ground that lets you run tests in an actual browser with minimal outside dependencies. As a beginner or a large enterprise project, you're likely better off using Cypress, but advanced users with smaller teams can be more nimble with a Mocha/Puppeteer setup.

Confused by Puppeteer's async/await API? My new ebook, Mastering Async/Await, is designed to give you an integrated understanding of async/await fundamentals and how async/await fits in the JavaScript ecosystem in a few hours. Get your copy!