The content of this article involves ES6 Async, JEST related knowledge, readers who are not familiar with the above content can understand the relevant content first.

What is the Puppeteer

It is provided by the Chrome team and provides a set of apis in the Node layer to control Chrome or Chromium through the Devtools protocol, which means that we can write code in the Node environment to control the behavior of the browser. It allows us to do the following:

  • Generate page snapshots: images, PDFS

  • Grab the SPA app to generate a pre-rendered page

  • Automated form submission, UI testing, keyboard entry

  • Capture application performance Data (Chrome Performance Timeline)

  • Testing chrome extensions

Of course, these behaviors are a small part of puppeteer’s capabilities, and more readers can read the Puppeteer documentation to learn more about puppeteer testing practices in the E2E

E2E test

To put it simply, it is to simulate real user scenarios for testing, and the application is expected to respond to user operations normally. The key point is to simulate the user environment and simulate user operations.

So for a Web application, the user environment is the browser, and the user actions are mostly moving and clicking, and that’s the part that we need to simulate, so let’s go straight to the environment and practice.

Part of the environment

This example uses puppeteer jest jest- Puppeteer in MAC environment

The above dependencies need to be installed first. Note the following:

  • Puppeteer downloads chromium by default. You can skip the chromium download by running export PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true

  • Node versions older than 8 are preferred

Next, the configuration is explained

First comes the jest related configuration, creating jest.config.js in the root directory

//jest.config.js
const config = require('config');
const _ = require('lodash');

module.exports = {
    preset: 'jest-puppeteer'./ / call the preset
    globals: _.assign({}, config.get('e2e.variable'), { // Global variables can be injected here
        ENV_URL: config.get('baseUrl')}),testMatch: ['**/__e2e__/**/*.test.js? (x)'] // Specify the file to test
};
Copy the code

The next step is to configure the puppeteer, creating jest-puppeteer.config.js in the root directory

//jest-puppeteer.config.js
module.exports = {
    launch: {
      	headless: true// Set the running mode,falseWill work in mode with GUI interface,trueDo not enable the GUI interface executablePath: // Set the local Chrome path. Chrome Canary is recommended'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'}};Copy the code

The practice part

With the environment configured, it’s time to get into practice. For most systems, especially management systems, the first step is to login to the system, so our first practice is to simulate a login using puppeteer scripts. The example here is based on a system login process from the authors.

const loginFunc = page => async () => { await page.setViewport({ width: 1280, height: 720 }); // Set window size await page.goto('${ENV_URL}/login`); Const loginIframe = page.frames().find(f => f.name() ==='login-iframe'); Login / / find the iframe await loginIframe. WaitForSelector ('#login-tab-container > div.tab-item.tab-right'
    ); 
    await loginIframe.click('#login-tab-container > div.tab-item.tab-right'); // switch to TAB await page.waitfor (1000); / / special purpose, after that const username = await loginIframe. WaitForSelector ('#username'); // find the input box await username. Type (username); / / input user name const pass = await loginIframe. WaitForSelector ('#password'); await pass.type(PASSWORD); // Enter password await loginifame.click ('#login-btn'); // Click await page.waitforNavigation (); // Wait for the jump to complete}; module.exports = { loginFunc: loginFunc };Copy the code

In this example, the process of opening the login page, entering the user name, password, and login is shown. It mainly involves setViewport Goto Frames waitForSelector click waitFor Type waitForNavigation apis. Here are some of the more important apis. For more information, please refer to the official documents.

setViewport sets the window size and returns a PromisewaitThe ForSelector argument is a CSS Selector, which returns a promise. Resolve will not be resolved until the specified element appears, reject will be rejected after the timeouttypeEnter something into an element and return a promisewaitForNavigation fires when the URL changes, returns a promise, and resolves when the navigation endshistoryThis method can also be used to wait for the end of the navigation frames when the API makes URL changes to get all iframes in the pageCopy the code

After reading this example, you will find that the PUPpeteer API design is very semantic and understandable. But in fact, there are several pit points in this example. Here is the experience gained by the author from this example:

  • WaitForSelector works only if the target Selector doesn’t already exist in the DOM, if the target element is called display: None; Visibility: hidden Toggles this method directly to resolve.

  • If there is an animation during the toggle, the action on the elements in the click or type animation will not take effect until the animation ends, so in this example we added waitFor(1000) to waitFor the animation to finish

Now that we have logged in, we are ready to automate our tests with JEST. Here is an example from this section:

const utils = require('./utils');

jest.setTimeout(10000);

describe('e2e test', () => {
    beforeAll(utils.loginFunc(page));
    it('e2e test-1', async () => {
        const el = await page.waitForSelector(
            '#root > .services_C2FC97 > .ant-row > .ant-col-8:nth-child(1) > .service-card_C2FC97'
        );
        expect(await el.$eval('p', node => node.innerText)).toBe('test'); // get innerText of specified element}); });Copy the code

In this example, we first introduce the loginFunc written above as a pre-condition for each test. Due to the use of jest-Puppeteer, puppeteer page and Browser objects are automatically injected into the runtime environment, so they can be called directly. This example tests whether a feature card on the front page is titled Test; This example is very basic, but I believe that from the login example above, the reader has seen the routine of writing E2E test scripts.

Jest. SetTimeout (ms) is required to extend the puppeteer test execution time, because puppeteer startup time and page opening time are difficult to estimate. Otherwise, the puppeteer test may fail due to timeout.

Yarn E2E YARN Run V1.9.4$ cross-env NODE_ENV=test jest -c jest.config.jsDetermining test suites to run... DevTools listening on the ws: / / 127.0.0.1:54751 / DevTools FFC/browser / 7 d8d289a - 5335-4 - a65c - f7e98cb34de0 [1129/154120.584773: WARNING: spdy_session. Cc (3152)] Received HEADERS for invalid stream PASS __e2e__ 25 / demo test. Js (6.931s) e2E test ✓ e2E test-1 (1540ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 Total Time: 7.046s Ran all test suites. ✨ Done in 10.72s.Copy the code

So that’s the end of the tutorial, does it feel like a button Sprite? However, puppeteer allows for much more powerful controls, such as listening for network requests and page creation, that can be achieved manually in the browser, as well as through the Node API provided by puppeteer.

So what does E2E testing do for us? In the author’s opinion, for a long-term iterative project, with the expansion of the project scale, the time spent in functional regression testing will inevitably increase. In this case, if automated testing can help us carry out part of the test, our efficiency will be greatly improved. At the same time, periodic test tasks can be used to find the problems in product functions earlier, so as to ensure that products can be delivered on time, which is the biggest value it brings.

Puppeteer-recorder is a Chrome plugin that allows you to easily record test scripts without having to manually write scripts 😄 (due to the same origin policy, this tool does not support recording operations within iframe).

@author: Monado