mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 14:03:30 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			168 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			168 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
# Web frontend black-box Puppeteer tests
 | 
						|
 | 
						|
While our [node test suite](testing-with-node.md) 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:
 | 
						|
 | 
						|
```bash
 | 
						|
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`.
 | 
						|
 | 
						|
## How Puppeteer tests work
 | 
						|
 | 
						|
The Puppeteer tests use a real Chromium browser (powered by
 | 
						|
[puppeteer](https://github.com/puppeteer/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:
 | 
						|
 | 
						|
```js
 | 
						|
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][learn-async-await].
 | 
						|
 | 
						|
## Debugging Puppeteer tests
 | 
						|
 | 
						|
The following questions are useful when debugging Puppeteer test
 | 
						|
failures you might see in [continuous
 | 
						|
integration](continuous-integration.md):
 | 
						|
 | 
						|
- Does the flow being tested work properly in the Zulip browser UI?
 | 
						|
  Test failures can reflect real bugs, and often it's easier and more
 | 
						|
  interactive to debug an issue in the normal Zulip development
 | 
						|
  environment than in the Puppeteer test suite.
 | 
						|
- Does the change being tested adjust the HTML structure in a way that
 | 
						|
  affects any of the selectors used in the tests? If so, the test may
 | 
						|
  just need to be updated for your changes.
 | 
						|
- Does the test fail deterministically when you run it locally using
 | 
						|
  e.g., `./tools/test-js-with-puppeteer compose.ts`? If so, you can
 | 
						|
  iteratively debug to see the failure.
 | 
						|
- Does the test fail nondeterministically? If so, the problem is
 | 
						|
  likely that a `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.
 | 
						|
- Does the test fail when you are typing (filling the form) on a modal
 | 
						|
  or other just-opened UI widget? Puppeteer starts typing after focusing on
 | 
						|
  the text field, sending keystrokes one after another. So, if
 | 
						|
  application code explicitly focuses the modal after it
 | 
						|
  appears/animates, this could cause the text field that Puppeteer is
 | 
						|
  trying to type into to lose focus, resulting in partially typed data.
 | 
						|
  The recommended fix for this is to wait until the modal is focused before
 | 
						|
  starting typing, like this:
 | 
						|
  ```JavaScript
 | 
						|
  await page.waitForFunction(":focus").attr("id") === modal_id);
 | 
						|
  ```
 | 
						|
 | 
						|
These tools/features are often useful when debugging:
 | 
						|
 | 
						|
- You can use `console.log` statements both in Puppeteer tests and the
 | 
						|
  code being tested to print-debug.
 | 
						|
- Zulip's Puppeteer tests are configured to generate screenshots of
 | 
						|
  the state of the test browser when an assert statement fails; these
 | 
						|
  are stored under `var/puppeteer/*.png` and are extremely helpful for
 | 
						|
  debugging test failures.
 | 
						|
- TODO: Mention how to access Puppeteer screenshots in CI.
 | 
						|
- TODO: Add an option for using the `headless: false` debugging mode
 | 
						|
  of Puppeteer so you can watch what's happening, and document how to
 | 
						|
  make that work with Vagrant.
 | 
						|
- TODO: Document `--interactive`.
 | 
						|
- TODO: Document how to run 100x in CI to check for nondeterministic
 | 
						|
  failures.
 | 
						|
- TODO: Document any other techniques/ideas that were helpful when porting
 | 
						|
  the Casper suite.
 | 
						|
- The Zulip server powering these tests is just `run-dev` with some
 | 
						|
  extra [Django settings](../subsystems/settings.md) 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](https://github.com/puppeteer/puppeteer#debugging-tips); some
 | 
						|
tips may require temporary patches to functions like `run_test` or
 | 
						|
`ensure_browser` in `web/e2e-tests/lib/common.ts`.
 | 
						|
 | 
						|
## 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:
 | 
						|
 | 
						|
- Run just the file containing your new tests as described above to
 | 
						|
  have a fast debugging cycle.
 | 
						|
- When you're done writing a test, run it 100 times in a loop to
 | 
						|
  verify it does not fail nondeterministically (see above for notes on
 | 
						|
  how to get CI to do it for you); this is important to avoid
 | 
						|
  introducing extremely annoying nondeterministic failures into
 | 
						|
  `main`.
 | 
						|
- With black-box browser tests like these, it's very important to write your code
 | 
						|
  to wait for browser's UI to update before taking any action that
 | 
						|
  assumes the last step was processed by the browser (e.g., after you
 | 
						|
  click on a user's avatar, you need an explicit wait for the profile
 | 
						|
  popover to appear before you can try to click on a menu item in that
 | 
						|
  popover). This means that before essentially every action in your
 | 
						|
  Puppeteer tests, you'll want to use `waitForSelector` or similar
 | 
						|
  wait function to make sure the page or element is ready before you
 | 
						|
  interact with it. The [puppeteer docs site](https://pptr.dev/) is a
 | 
						|
  useful reference for the available wait functions.
 | 
						|
- When using `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.
 | 
						|
- The test suite uses a smaller set of default user accounts and other
 | 
						|
  data initialized in the database than the normal development
 | 
						|
  environment; specifically, it uses the same setup as the [backend
 | 
						|
  tests](testing-with-django.md). To see what differs from
 | 
						|
  the development environment, check out the conditions on
 | 
						|
  `options["test_suite"]` in
 | 
						|
  `zilencer/management/commands/populate_db.py`.
 | 
						|
 | 
						|
[learn-async-await]: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await
 |