# Screenshot Testing Design Document ## 1. Problem Statement Topola Viewer is a highly interactive, visual genealogy exploration tool that renders family trees using complex SVG layouts and D3 configurations. As the codebase evolves, minor updates to CSS styles, React components, or underlying layout algorithms can easily introduce subtle visual regressions—such as overlapping text labels, misaligned parent-child connector lines, or broken formatting in the side panels—that standard text-based DOM tests cannot detect. To prevent these visual bugs from reaching production, we are introducing an automated screenshot (visual regression) testing suite using Playwright. This testing suite will capture pixel-perfect snapshots of critical interface states, automatically flag unintended visual changes, and guarantee a consistently polished, premium user experience across all releases. ## 2. The Technical Plan To consistently verify the user interface without introducing complex setups, the screenshot testing framework is built on a local-first, self-contained execution model. It operates by launching a virtual web browser, running the Topola Viewer application inside it, and checking it against stored master images (baselines). This setup consists of four major parts working in harmony: 1. **The Test Orchestrator (Playwright):** This acts as the central manager. It starts our local web server, launches virtual browser instances, automates user actions (such as clicking buttons or navigation links), captures the screenshots, and does the pixel-by-pixel comparison against our baseline master images. 2. **The Local Web Server:** A background web server hosting the Topola Viewer application code. It serves the frontend interface directly to the virtual browser so that the test context runs identically to our actual user deployments. 3. **The Network Traffic Controller (Route Interceptor):** An in-memory network router managed by the orchestrator. When the browser attempts to download a genealogy file (e.g., `family.ged`) or load a person's photo, the router intercepts that request and immediately answers it with tiny, predefined test datasets (fixtures). This guarantees that the test runs completely offline, remains blazingly fast, and has absolute visual predictability. 4. **The Environment Sanitizer:** A tiny automated script executed directly inside the browser window right before a screenshot is snapped. Its only job is to locate and overwrite dynamic or shifting text elements (like Git commit hashes or changelog dates) with fixed placeholders, ensuring they do not trigger false test failures. ## 3. Alternatives Considered & Rejected To prevent future developer friction, avoid redundant debugging cycles, and establish firm design guardrails, the following technical alternatives were evaluated and rejected: ### Alternative A: Global Animation Freezing (`freeze=true` Query Parameter) * **Considered:** Forcing Topola's SVG engine to completely freeze all animations globally in E2E tests to prevent visual capturing mismatches. * **Why Rejected:** Topola Viewer's initial chart mounting is entirely static; D3 transitions are only triggered during interactive navigation (e.g., clicking to shift focus to a child node). Since the target snapshots capture the initial mount of a chart or isolated panel element, introducing a complex global animation freezing hook is redundant. Instead, utilizing Playwright's standard auto-waiting mechanism (which pauses until the SVG is fully loaded and stationary) provides flawless, stable captures naturally. ### Alternative B: Monolithic Reference GEDCOM File (`rich_details.ged`) * **Considered:** Maintaining a single, massive master `.ged` file containing a wide collection of custom individuals (with complex names, nested attributes, attached photos, and custom events) to serve all tests. * **Why Rejected:** Monolithic test fixtures introduce severe coupling and high maintenance overhead. If a developer tweaks a birth record to debug an event-layout test, it can unintentionally shift elements in unrelated parts of the tree, failing baselines for name-formatting or photo rendering. Instead, creating microscopic, ad-hoc GEDCOM strings inline within each test case guarantees complete visual isolation, makes test intents instantly readable, and speeds up parsing. ### Alternative C: Build-Time Environment Variable Overrides (Git SHA/Time) * **Considered:** Overriding `VITE_GIT_TIME` and `VITE_GIT_SHA` at build time specifically for E2E testing. * **Why Rejected:** In production gating pipelines (such as GitHub Actions), the application is built and packaged into production-ready assets before the E2E job begins execution. Re-compiling Vite assets solely to inject static E2E values is slow, resource-intensive, and violates the rule of testing the exact binary that will be deployed. Instead, executing an in-browser DOM override (`page.evaluate`) right before screenshot execution is lightweight, self-contained, and requires zero alterations to the build flow or production bundle. ### Alternative D: Strict Pixel-Perfect Matching (Zero-Tolerance) * **Considered:** Requiring absolute, 100% visual equivalence with zero pixel mismatch allowed. * **Why Rejected:** Slight discrepancies in font rendering, subpixel antialiasing, and color blending are unavoidable across different operating systems (macOS developers vs. Linux CI agents). Enforcing zero-tolerance leads to extremely brittle tests that fail constantly due to harmless system-level rendering differences. Instead, setting relaxed thresholds (`maxDiffPixelRatio: 0.05` and `threshold: 0.2`) filters out system noise while aggressively catching genuine layout bugs, overlapping elements, and formatting failures. ## 4. Detailed Implementation Plan This section defines the granular, step-by-step implementation steps and enumerates every file that will be created or modified to complete this visual regression framework. ### A. Enumeration of Files #### 1. Files to [MODIFY] * **[playwright.config.ts](../playwright.config.ts)** * *Rationale:* Isolate visual regression tests into a separate Playwright project (separate from standard functional E2E tests). This allows applying dedicated visual settings (like viewport locking, automatic scrollbar hiding, and custom screenshot mismatch thresholds) exclusively to visual tests without polluting standard E2E runs. Threshold settings are configured globally under `expect.toHaveScreenshot`. * **[package.json](../package.json)** * *Rationale:* Add dedicated npm script commands to target the standard E2E project (`--project=e2e`) and the isolated visual testing project (`--project=visual`), preventing slow screenshot tests from bloating standard developer verification cycles. #### 2. Files to [NEW] * **`tests/helpers.ts`** * *Rationale:* Provide shared E2E/visual testing helper utilities. Features `setupHermeticEnvironment()` to abort external tracking requests and embed local fonts (ensuring offline hermetic execution) and `setupGedcomRoute()` to serve a standard mock `.ged` dataset. * **`tests/intro_visual.spec.ts`** * *Rationale:* Verify the landing page layout, copy block positions, and logo alignments. Employs an in-browser DOM script to overwrite dynamic footer versioning and dynamic changelog blocks prior to capture, ensuring baseline immunity. * **`tests/charts_visual.spec.ts`** * *Rationale:* Verify chart canvas boundaries, nodes, colors, and connections. Iterates over three of the supported layouts (`Hourglass`, `Relatives`, `Donatso`) using a simple tree, and captures screenshots of the stabilized D3 canvas. * **`tests/details_visual.spec.ts`** * *Rationale:* Verify details panel formats, image margins, fact headers, and sources. Defines tiny, ad-hoc mock GEDCOM inline strings for individual edge cases (long multi-part names, attached images, nested events) and serves pre-existing photo assets (e.g. `docker/examples/photos/photos/I1.jpg`) to render photos without broken image layouts. * **`tests/config_visual.spec.ts`** * *Rationale:* Verify the visual synchronization between Side Panel settings checkboxes/radio inputs and the SVG canvas. Captures full-viewport screenshots (at 1280x720) across the three curated configuration combinations. ### B. Step-by-Step Execution Plan #### Step 1: Visual Project Isolation & Script Provisioning 1. Open `playwright.config.ts` and configure separate projects within the projects array: * Define an `e2e` project using desktop Chrome settings that matches all `.spec.ts` files (`testMatch`) but excludes `*_visual.spec.ts` files (`testIgnore`). * Define a dedicated `visual` project that matches only `*_visual.spec.ts` files (`testMatch`), and locks the browser viewport to a width of `1280` and height of `720` pixels in the `use` configuration. 2. Configure custom visual expectation thresholds globally under `expect.toHaveScreenshot` (specifically setting `maxDiffPixelRatio` to `0.05`, `threshold` to `0.2`, and `animations` to `'disabled'`). 3. Open `package.json` and update the scripts to target standard and visual projects respectively: * `"test:e2e": "playwright test --project=e2e"` to run functional E2E tests exclusively. * `"test:visual": "playwright test --project=visual"` to run visual regression tests exclusively. * `"test:visual:update": "playwright test --project=visual --update-snapshots"` to automatically regenerate baseline reference files. #### Step 2: Landing Page Visual Validation Spec (`tests/intro_visual.spec.ts`) 1. Define a test block marked with the `@visual` tag, utilizing `setupHermeticEnvironment` helper in `beforeEach`. 2. Instruct the browser to navigate to the root path `/`. 3. Right before assertion, trigger `page.evaluate` to clean dynamic elements: * Target the `.version` class element and set `.innerText = "version: 2026-01-01 00:00 (testcommit)"`. * Target the changelog element (the container immediately following the "What's new" heading) and replace its HTML with a static placeholder change entry. 4. Snap the screenshot using `expect(page).toHaveScreenshot('intro-page.png')`. #### Step 3: Core SVG Canvas Layouts Spec (`tests/charts_visual.spec.ts`) 1. Set up a `beforeEach` block that initializes `setupGedcomRoute(context)` from `helpers.ts` to intercept `**/family.ged` requests and fulfill them with raw GEDCOM test data. 2. Write tests with the `@visual` tag iterating through the 3 supported layouts (`hourglass`, `relatives`, `donatso`): * Set browser route to `/#/view?url=https://example.org/family.ged&view=[hourglass|relatives|donatso]`. * Determine the appropriate container selector: `#dotatsoSvgContainer` if the view is `donatso`, otherwise `#svgContainer`. * Locate the container element, and call `locator.waitFor()` to ensure the element is fully attached and visible. * Wait for D3 rendering and layout stabilization using a brief layout-specific timeout (`waitTime`: `500ms` for hourglass/relatives, `1500ms` for donatso). * Capture the isolated canvas screenshot: `expect(container).toHaveScreenshot('chart-[type].png')`. #### Step 4: Details Panel Layouts Spec (`tests/details_visual.spec.ts`) 1. Set up a `beforeEach` block to establish hermetic routes via `setupHermeticEnvironment(context)`. 2. Define isolated test blocks with the `@visual` tag, each loading its own dedicated inline micro-GEDCOM dataset: * **Complex Names Test:** * Mock `**/family.ged` with a GEDCOM string containing prefix/suffix/rufname tags. * Navigate to the view route with `sidePanel=true`, locate the side panel container `#sidebar`. * Assert sidebar visual representation: `expect(page.locator('#sidebar')).toHaveScreenshot('details-complex-name.png')`. * **Image / Photo Rendering Test:** * Mock `**/family.ged` containing an `OBJE` tag pointing to a photo path (e.g. `photos/I1.jpg`). * Intercept requests for `**/photos/I1.jpg` and fulfill the request by serving the project asset `docker/examples/photos/photos/I1.jpg`. * Navigate, wait for the image load handler to complete (`img.waitFor({state: 'visible'})` and checking `image.complete` status). * Assert sidebar visual representation: `expect(page.locator('#sidebar')).toHaveScreenshot('details-photo-render.png')`. * **Custom Facts & Citations Test:** * Mock `**/family.ged` containing complex nested fact (`FACT`), source (`SOUR`), and note (`NOTE`) trees. * Select the individual and wait for `#sidebar` to load. * Assert sidebar visual representation: `expect(page.locator('#sidebar')).toHaveScreenshot('details-events-sources.png')`. * **Immediate Family Rendering Test:** * Mock `**/family.ged` containing an individual with explicit parental links (`FAMC`) and multi-partner spousal families (`FAMS`) to display biological parents, spouses, and chronologically sorted children blocks. * Select the individual and wait for `#sidebar` to load. * Assert sidebar visual representation: `expect(page.locator('#sidebar')).toHaveScreenshot('details-immediate-family.png')`. #### Step 5: Configurations Integration Spec (`tests/config_visual.spec.ts`) 1. Define a test block tagged `@visual` with a locked browser window viewport size of `1280x720` via `playwright.config.ts`. 2. In `beforeEach`, mock `**/family.ged` using `setupGedcomRoute(context)`, load `/view?sidePanel=true`, wait for `#sidebar` and `#content` to be visible, and click the "Settings" tab (`await page.getByText('Settings', {exact: true}).click();`) to expose config fields. 3. Assert the **Default Configuration (State 1)**: * Verify that both the checkbox states and the corresponding generation-colored SVG boxes are in alignment. * Assert the entire integrated screen: `expect(page).toHaveScreenshot('config-state-default.png')`. 4. Automate panel clicks: Scope locators using `page.locator('form.details .item')` to target the "Colors" and "IDs" section items. Select the "by sex" color radio button and select the "hide" IDs option. 5. Wait for updates (`page.waitForTimeout(300)`) and assert the **Sex Colors & No IDs Configuration (State 2)**: * Assert the entire integrated screen: `expect(page).toHaveScreenshot('config-state-gender-no-ids.png')`. 6. Automate panel clicks: Scope locators using `page.locator('form.details .item')` to target the "Colors" and "Sex" section items. Select the "none" color radio button and select the "hide" sex option. 7. Wait for updates (`page.waitForTimeout(300)`) and assert the **Minimalist Configuration (State 3)**: * Assert the entire integrated screen: `expect(page).toHaveScreenshot('config-state-minimalist.png')`. ## 5. CI/CD Pipeline Integration To ensure that no visual regressions are introduced into the master branch, the visual testing suite is integrated into the GitHub Actions CI/CD workflow ([node.js.yml](../.github/workflows/node.js.yml)) alongside existing tests. ### Pipeline Configuration Visual tests run sequentially after standard E2E tests. The workflow executes the following steps: 1. **Install Dependencies:** Resolves Node.js package dependencies and installs/caches Playwright browser binaries (specifically `chromium`). 2. **Run E2E Tests:** Runs standard functional tests using `npm run test:e2e`. 3. **Run Visual Tests:** Runs visual regression tests using `npm run test:visual`. ### Playwright HTML Reports in CI To prevent test reports from overwriting each other, the HTML reports for the different testing suites are output to distinct subdirectories within the `playwright-report` folder: * **E2E Tests:** Saved to `playwright-report/e2e` by setting the `PLAYWRIGHT_HTML_REPORT` environment variable. * **Visual Tests:** Saved to `playwright-report/visual` by setting the `PLAYWRIGHT_HTML_REPORT` environment variable. Both reports are bundled and uploaded as a single workflow artifact (`playwright-report-${{ matrix.node-version }}`) on completion, allowing easy review of failures.