docs/testing/testing-with-puppeteer.md
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.).
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 web/e2e-tests and make use
of various useful helper functions defined in
web/e2e-tests/lib/common.ts.
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 direct 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.
The following questions are useful when debugging Puppeteer test failures you might see in continuous integration:
./tools/test-js-with-puppeteer compose.ts? If so, you can
iteratively debug to see the failure.waitForSelector statement is either missing or not
waiting for the right thing. Tests fail nondeterministically much
more often on very slow systems like those used for Continuous
Integration (CI) services because small races are amplified in those
environments; this often explains failures in CI that cannot be
easily reproduced locally.await page.waitForFunction(":focus").attr("id") === modal_id);
These tools/features are often useful when debugging:
console.log statements both in Puppeteer tests and the
code being tested to print-debug.var/puppeteer/*.png and are extremely helpful for
debugging test failures.headless: false debugging mode
of Puppeteer so you can watch what's happening, and document how to
make that work with Vagrant.--interactive.run-dev with some
extra Django settings from
zproject/test_extra_settings.py to configure an isolated database
so that the tests will not interfere/interact with a normal
development environment. The console output while running the tests
includes the console output for the server; any Python exceptions
are likely actual bugs in the changes being tested.See also Puppeteer upstream's debugging
tips; some
tips may require temporary patches to functions like run_test or
ensure_browser in web/e2e-tests/lib/common.ts.
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:
main.waitForSelector or similar
wait function to make sure the page or element is ready before you
interact with it. The puppeteer docs site is a
useful reference for the available wait functions.waitForSelector, you always want to use the
{visible: true} option; otherwise the test will stop waiting as
soon as the target selector is present in the DOM even if it's
hidden. For the common UI pattern of having an element always be
present in the DOM whose presence is managed via show/hide rather
than adding/removing it from the DOM, waitForSelector without
visible: true won't wait at all.options["test_suite"] in
zilencer/management/commands/populate_db.py.