--- name: visual-test description: "Visually verify UI changes using Puppeteer screenshots. Use when you need to check layout, colors, positioning, or other visual aspects of a UI change." --- # Visual Test Runs a real browser against the Zulip test server and takes screenshots you can read as images to verify layout, colors, positioning, text content, etc. ## Steps ### 1. Write the puppeteer test script Create `web/e2e-tests/_claude__test.test.ts` using this template: ```typescript import type {Page} from "puppeteer"; import * as common from "./lib/common.ts"; async function visual_test(page: Page): Promise { await common.log_in(page); await common.screenshot(page, "step-1-logged-in"); // Navigate, interact, and screenshot each significant state. // See "Available helpers" below. } await common.run_test(visual_test); ``` Adapt the body to exercise whatever UI you need to verify. Take a screenshot at every visually significant state using descriptive names like `step-2-color-picker-open`, `step-3-color-selected`. **Important patterns:** These patterns are derived from the existing Puppeteer tests in `web/e2e-tests/`. Follow them to write reliable, non-flaky tests. #### Waiting: never use hardcoded timeouts The existing test suite has essentially zero `setTimeout` calls (the two in `common.ts` are explicitly commented workarounds for specific animation flakes). Always wait for the specific condition you expect instead. The three main waiting primitives, in order of preference: - **`waitForSelector`** — wait for an element to appear or disappear. This is the most common pattern in the test suite (100+ uses): ```typescript // Wait for element to be visible (most common) await page.waitForSelector("#left-sidebar", {visible: true}); // Wait for element to disappear (e.g., overlay closed, row deleted) await page.waitForSelector("#subscription_overlay", {hidden: true}); ``` - **`waitForFunction`** — wait for a condition that can't be expressed as a single selector (text content, element count, attribute value, application state): ```typescript // Wait for specific text content await page.waitForFunction( () => document.querySelector(".save-button")?.textContent?.trim() === "Save changes", ); // Wait for element count after filtering await page.waitForFunction( () => document.querySelectorAll(".linkifier_row").length === 4, ); // Wait for an input's value to update await page.waitForFunction( () => document.querySelector("#full_name")?.value === "New name", ); // Wait for focus to land on a specific element await page.waitForFunction( () => document.activeElement?.classList?.contains("search") === true, ); // Wait for internal app state via zulip_test await page.waitForFunction( (content) => { const last_msg = zulip_test.current_msg_list?.last(); return last_msg !== undefined && last_msg.raw_content === content && !last_msg.locally_echoed; }, {}, content, ); ``` - **`waitForNavigation`** — only for actual full-page navigations (form submits, reloads). Wrap with `Promise.all` when the navigation is triggered by an action: ```typescript await Promise.all([ page.waitForNavigation(), page.$eval("form#login_form", (form) => { form.submit(); }), ]); ``` #### Interacting with elements - **`page.click(selector)`** is the standard for clicking. When it's unreliable (overlapping elements, timing), fall back to clicking via `evaluate` — several existing tests do this with a comment explaining why: ```typescript // When page.click() is unreliable, click via the DOM directly await page.evaluate(() => { document.querySelector(".dialog_submit_button")?.click(); }); ``` - **`page.type(selector, text)`** for typing. Use `{delay: 100}` when typing triggers a typeahead or filter that needs per-keystroke updates: ```typescript await page.type('[name="user_list_filter"]', "ot", {delay: 100}); ``` - **`common.clear_and_type(page, selector, text)`** to replace existing input content (triple-click + Delete + type). - **`common.fill_form(page, selector, params)`** to fill multiple form fields at once — handles text inputs, checkboxes (by toggling), and `