Web frontend black-box Puppeteer tests

While our node test suite is the preferred way to test most frontend code because they are easy to write and maintain, some code is best tested in a real browser, either because of navigation (E.g. login) or because we want to verify the interaction between Zulip logic and browser behavior (E.g. copy/paste, keyboard shortcuts, etc.).

Running tests

You can run this test suite as follows:

tools/test-js-with-puppeteer

See tools/test-js-with-puppeteer --help for useful options, especially running specific subsets of the tests to save time when debugging.

The test files live in frontend_tests/puppeteer_tests and make use of various useful helper functions defined in frontend_tests/puppeteer_lib/common.js.

How Puppeteer tests work

The Puppeteer tests use a real Chromium browser (powered by puppeteer), connected to a real Zulip development server. These are black-box tests: Steps in a Puppeteer test are largely things one might do as a user of the Zulip web app, like "Type this key", "Wait until this HTML element appears/disappears", or "Click on this HTML element".

For example, this function might test the x keyboard shortcut to open the compose box for a new private message:

async function test_private_message_compose_shortcut(page) {
    await page.keyboard.press("KeyX");
    await page.waitForSelector("#private_message_recipient", {visible: true});
    await common.pm_recipient.expect(page, "");
    await close_compose_box(page);
}

The test function presses the x key, waits for the #private_message_recipient input element to appear, verifies its content is empty, and then closes the compose box. The waitForSelector step here (and in most tests) is critical; tests that don't wait properly often fail nonderministically, because the test will work or not depending on whether the browser updates the UI before or after executing the next step in the test.

Black-box tests are fantastic for ensuring the overall health of the project, but are also slow, costly to maintain, and require care to avoid nondeterministic failures, so we usually prefer to write a Node test instead when both are options.

They also can be a bit tricky to understand for contributors not familiar with async/await.

Debugging Puppeteer tests

The following questions are useful when debugging Puppeteer test failures you might see in continuous integration:

These tools/features are often useful when debugging:

See also Puppeteer upstream's debugging tips; some tips may require temporary patches to functions like run_test or ensure_browser in frontend_tests/puppeteer_lib/common.js.

Writing Puppeteer tests

Probably the easiest way to learn how to write Puppeteer tests is to study some of the existing test files. There are a few tips that can be useful for writing Puppeteer tests in addition to the debugging notes above: