From d427300ddf8f3b0198f1464269872ece64577bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemek=20Wi=C4=99ch?= Date: Wed, 6 May 2026 11:51:30 +0200 Subject: [PATCH] Migrate from Cypress to Playwright --- .github/workflows/deploy-gh-pages.yml | 29 +- .github/workflows/deploy-wikitree-apps.yml | 29 +- .github/workflows/node.js.yml | 37 +- .gitignore | 6 +- PROJECT_STRUCTURE.md | 6 +- cypress.config.ts | 11 - cypress/e2e/README.md | 17 - cypress/e2e/chart_view.cy.js | 19 - cypress/e2e/embedded.cy.js | 11 - cypress/e2e/intro.cy.js | 12 - cypress/e2e/search.cy.js | 12 - cypress/e2e/webmcp.cy.js | 37 - docs/PLAYWRIGHT_DESIGN.md | 267 ++++ jest.config.ts | 1 + package-lock.json | 1337 ++------------------ package.json | 15 +- playwright.config.ts | 32 + screenshot.png | Bin 87318 -> 0 bytes src/app.tsx | 2 +- tests/chart_view.spec.ts | 26 + tests/embedded.spec.ts | 33 + tests/fixtures/embedded_frame.html | 44 + tests/global.d.ts | 3 + tests/helpers.ts | 33 + tests/intro.spec.ts | 18 + tests/search.spec.ts | 22 + tests/tsconfig.json | 11 + tests/webmcp.spec.ts | 73 ++ 28 files changed, 740 insertions(+), 1403 deletions(-) delete mode 100644 cypress.config.ts delete mode 100644 cypress/e2e/README.md delete mode 100644 cypress/e2e/chart_view.cy.js delete mode 100644 cypress/e2e/embedded.cy.js delete mode 100644 cypress/e2e/intro.cy.js delete mode 100644 cypress/e2e/search.cy.js delete mode 100644 cypress/e2e/webmcp.cy.js create mode 100644 docs/PLAYWRIGHT_DESIGN.md create mode 100644 playwright.config.ts delete mode 100644 screenshot.png create mode 100644 tests/chart_view.spec.ts create mode 100644 tests/embedded.spec.ts create mode 100644 tests/fixtures/embedded_frame.html create mode 100644 tests/global.d.ts create mode 100644 tests/helpers.ts create mode 100644 tests/intro.spec.ts create mode 100644 tests/search.spec.ts create mode 100644 tests/tsconfig.json create mode 100644 tests/webmcp.spec.ts diff --git a/.github/workflows/deploy-gh-pages.yml b/.github/workflows/deploy-gh-pages.yml index a7ae42c..d9e0e21 100644 --- a/.github/workflows/deploy-gh-pages.yml +++ b/.github/workflows/deploy-gh-pages.yml @@ -18,7 +18,34 @@ jobs: - run: npm ci - run: npm run build - run: npm test - - run: npm run cy:start-and-run + - name: Get Playwright version + id: playwright-version + run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT + + - name: Cache Playwright Browsers + id: cache-playwright + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + + - name: Install Playwright System Dependencies + run: npx playwright install-deps chromium + + - name: Install Playwright Browsers (If Not Cached) + if: steps.cache-playwright.outputs.cache-hit != 'true' + run: npx playwright install chromium + + - name: Run E2E Tests + run: npm run test:e2e + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report-gh-pages + path: playwright-report/ + retention-days: 30 - name: Deploy run: | git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git diff --git a/.github/workflows/deploy-wikitree-apps.yml b/.github/workflows/deploy-wikitree-apps.yml index a21aa2e..b4015b2 100644 --- a/.github/workflows/deploy-wikitree-apps.yml +++ b/.github/workflows/deploy-wikitree-apps.yml @@ -18,7 +18,34 @@ jobs: - run: npm ci - run: npm run build - run: npm test - - run: npm run cy:start-and-run + - name: Get Playwright version + id: playwright-version + run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT + + - name: Cache Playwright Browsers + id: cache-playwright + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + + - name: Install Playwright System Dependencies + run: npx playwright install-deps chromium + + - name: Install Playwright Browsers (If Not Cached) + if: steps.cache-playwright.outputs.cache-hit != 'true' + run: npx playwright install chromium + + - name: Run E2E Tests + run: npm run test:e2e + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report-wikitree + path: playwright-report/ + retention-days: 30 - name: Install lftp run: sudo apt-get install -y lftp - name: Set up ssh diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 67ea166..4c8632e 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -28,10 +28,39 @@ jobs: cache: 'npm' - run: npm ci - name: Prettier Format Check - run: | - npm run prettier - git diff --exit-code + run: npx prettier --check "{src,tests}/**/*.{ts,tsx,json}" - run: npm run lint - run: npm run build - run: npm test - - run: npm run cy:start-and-run + + - name: Typecheck E2E Specs + run: npx tsc -p tests/tsconfig.json --noEmit + + - name: Get Playwright version + id: playwright-version + run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT + + - name: Cache Playwright Browsers + id: cache-playwright + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + + - name: Install Playwright System Dependencies + run: npx playwright install-deps chromium + + - name: Install Playwright Browsers (If Not Cached) + if: steps.cache-playwright.outputs.cache-hit != 'true' + run: npx playwright install chromium + + - name: Run E2E Tests + run: npm run test:e2e + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report-${{ matrix.node-version }} + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 2ce08e9..dc1deec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ dist node_modules src/react-app-env.d.ts -cypress/fixtures/example.json -cypress/screenshots -cypress/videos +playwright-report/ +test-results/ +.playwright/ .idea .vscode diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 68d63b8..83bf181 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -4,7 +4,7 @@ This directory is the root directory of the **Topola Genealogy Viewer** project. ## Purpose of this Directory -The root directory orchestrates the project. It contains configuration files for various tools (Vite, ESLint, Jest, Cypress, TypeScript), metadata about dependencies (`package.json`), and scripts for common tasks. It glues together the source code in `src`, the static assets in `public`, and the tests in `cypress`. +The root directory orchestrates the project. It contains configuration files for various tools (Vite, ESLint, Jest, Playwright, TypeScript), metadata about dependencies (`package.json`), and scripts for common tasks. It glues together the source code in `src`, the static assets in `public`, and the tests in `tests`. ## Files in this Directory (Categorized by Role) @@ -29,7 +29,7 @@ Here is an enumeration of the files in this directory, categorized by their role * **[tsconfig.json](tsconfig.json)**: Configuration file for the TypeScript compiler. ### Testing -* **[cypress.config.ts](cypress.config.ts)**: Configuration file for Cypress end-to-end testing framework. +* **[playwright.config.ts](playwright.config.ts)**: Configuration file for Playwright end-to-end testing framework. * **[jest.config.ts](jest.config.ts)**: Configuration file for the Jest testing framework used for unit tests. ### Documentation & Assets @@ -49,4 +49,4 @@ Based on the documentation in the subdirectories, here is a high-level overview * **[translations](src/translations)**: Holds localization JSON files. * **[util](src/util)**: Common utilities for dates, analytics, and data processing. * **[public](public)**: Contains static assets (like the favicon) served directly at the root path. -* **[cypress](cypress)**: Contains end-to-end tests in the `e2e` folder. +* **[tests](tests)**: Contains end-to-end tests. diff --git a/cypress.config.ts b/cypress.config.ts deleted file mode 100644 index b45ce7f..0000000 --- a/cypress.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from 'cypress' - -export default defineConfig({ - viewportWidth: 1280, - viewportHeight: 1024, - chromeWebSecurity: false, - e2e: { - baseUrl: 'http://localhost:3000/#', - supportFile: false, - }, -}) diff --git a/cypress/e2e/README.md b/cypress/e2e/README.md deleted file mode 100644 index de53a48..0000000 --- a/cypress/e2e/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Cypress E2E Tests - -This directory contains End-to-End (E2E) tests for the Topola Viewer application, using the Cypress testing framework. The purpose of these tests is to verify the application's behavior from a user's perspective by simulating interactions in a real browser environment. - -## Test Files - -- [chart_view.cy.js](file:///home/pwiech/personal/github/topola-viewer/cypress/e2e/chart_view.cy.js) - Tests the core functionality of the chart view. It verifies that data can be loaded from a remote URL, that the chart can be interacted with (e.g., expanding nodes to show more people), and that the side panel displays correct information. - -- [embedded.cy.js](file:///home/pwiech/personal/github/topola-viewer/cypress/e2e/embedded.cy.js) - Tests the application in embedded mode. It verifies that Topola can be successfully loaded and run within an iframe, which is how it might be used when embedded in other websites. - -- [intro.cy.js](file:///home/pwiech/personal/github/topola-viewer/cypress/e2e/intro.cy.js) - Tests the application's landing page (intro page). It ensures that the introductory text is visible and that the main menu actions (like opening a file or loading from a URL) are present. - -- [search.cy.js](file:///home/pwiech/personal/github/topola-viewer/cypress/e2e/search.cy.js) - Tests the search functionality within the application. It verifies that users can search for individuals and that selecting a result correctly highlights or displays that person in the view. diff --git a/cypress/e2e/chart_view.cy.js b/cypress/e2e/chart_view.cy.js deleted file mode 100644 index 1ed6f7c..0000000 --- a/cypress/e2e/chart_view.cy.js +++ /dev/null @@ -1,19 +0,0 @@ -describe('Chart view', () => { - beforeEach(() => { - cy.visit('/view?handleCors=false&url=https%3A%2F%2Fraw.githubusercontent.com%2FPeWu%2Ftopola%2Fmaster%2Fdemo%2Fdata%2Ffamily.ged'); - }); - - it('loads data from URL', () => { - cy.contains('Bonifacy'); - }); - - it('Animates chart', () => { - cy.contains('Chike').should('not.exist'); - cy.contains('Radobod').click({force: true}); - cy.contains('Chike'); - }); - - it('shows the right panel', () => { - cy.contains('a random note'); - }); -}); diff --git a/cypress/e2e/embedded.cy.js b/cypress/e2e/embedded.cy.js deleted file mode 100644 index d8c37d1..0000000 --- a/cypress/e2e/embedded.cy.js +++ /dev/null @@ -1,11 +0,0 @@ -describe('Embedded mode', () => { - it('shows data', () => { - const url = 'https://pewu.github.io/topola-embedded/#' + Cypress.config('baseUrl'); - cy.visit(url); - cy.get('iframe') - .its('0.contentDocument.body').should('not.be.empty') - .then(cy.wrap) - .find('#root') - .contains('Bonifacy'); - }); -}); diff --git a/cypress/e2e/intro.cy.js b/cypress/e2e/intro.cy.js deleted file mode 100644 index 5f61936..0000000 --- a/cypress/e2e/intro.cy.js +++ /dev/null @@ -1,12 +0,0 @@ -describe('Intro page', () => { - beforeEach(() => { - cy.visit('/'); - }); - it('displays intro text', () => { - cy.contains('Examples'); - }); - it('displays menu', () => { - cy.contains('Open file'); - cy.contains('Load from URL'); - }); -}); diff --git a/cypress/e2e/search.cy.js b/cypress/e2e/search.cy.js deleted file mode 100644 index e1a91f0..0000000 --- a/cypress/e2e/search.cy.js +++ /dev/null @@ -1,12 +0,0 @@ -describe('Chart view', () => { - beforeEach(() => { - cy.visit('/view?handleCors=false&url=https%3A%2F%2Fraw.githubusercontent.com%2FPeWu%2Ftopola%2Fmaster%2Fdemo%2Fdata%2Ffamily.ged'); - }); - it('Search works', () => { - cy.contains('Chike').should('not.exist'); - cy.get('#search').type('chik'); - cy.contains('Chike'); - cy.get('#search').type('{enter}'); - cy.get('#content').contains('Chike'); - }); -}); diff --git a/cypress/e2e/webmcp.cy.js b/cypress/e2e/webmcp.cy.js deleted file mode 100644 index fb110b4..0000000 --- a/cypress/e2e/webmcp.cy.js +++ /dev/null @@ -1,37 +0,0 @@ -describe('WebMCP Integration', () => { - let registeredTools = []; - - beforeEach(() => { - registeredTools = []; - cy.visit('/view?handleCors=false&url=https%3A%2F%2Fraw.githubusercontent.com%2FPeWu%2Ftopola%2Fmaster%2Fdemo%2Fdata%2Ffamily.ged', { - onBeforeLoad(win) { - win.navigator.modelContext = { - registerTool: (tool) => { - registeredTools.push(tool); - } - }; - } - }); - }); - - it('registers tools to standard modelContext', () => { - cy.wrap(registeredTools).should('have.length', 7); - const toolNames = registeredTools.map(t => t.name); - expect(toolNames).to.include('get_selected_person'); - expect(toolNames).to.include('search_indi'); - expect(toolNames).to.include('inspect_indi'); - expect(toolNames).to.include('focus_indi'); - expect(toolNames).to.include('find_relationship_path'); - expect(toolNames).to.include('get_ancestors'); - expect(toolNames).to.include('get_descendants'); - }); - - it('allows running focus_indi tool', () => { - cy.contains('Radobod'); - cy.wrap(registeredTools).then((tools) => { - const focusTool = tools.find(t => t.name === 'focus_indi'); - // Radobod's ID or another valid ID. In topola sample data let's just grab selection - expect(focusTool).to.not.be.undefined; - }); - }); -}); diff --git a/docs/PLAYWRIGHT_DESIGN.md b/docs/PLAYWRIGHT_DESIGN.md new file mode 100644 index 0000000..abff9e3 --- /dev/null +++ b/docs/PLAYWRIGHT_DESIGN.md @@ -0,0 +1,267 @@ +# Playwright E2E Testing Design Document + +## 1. Business & Functional Problem Statement + +Topola Viewer currently relies on a Cypress end-to-end (E2E) test suite to verify critical user flows, such as interactive chart rendering, search capabilities, and experimental WebMCP integrations. However, the existing setup is brittle and hard to maintain because it depends on live, external internet connections to fetch remote GEDCOM trees, uses loosely-typed JavaScript spec files, and suffers from complex process orchestration. Additionally, Cypress's historical architectural limitations make testing cross-origin iframe structures in embedded mode slow and difficult to simulate locally. Migrating this testing suite to Playwright in TypeScript will establish a fast, fully type-safe, hermetic, and self-contained validation pipeline, ensuring absolute quality control and deployment confidence. + +## 2. The Technical Plan + +The technical solution relies on transitioning our testing engine from Cypress to Playwright, utilizing a self-contained, local-first execution model. Instead of relying on external servers or separate terminal scripts to boot up our application, Playwright will orchestrate the entire lifecycle from start to finish. + +Here is a block diagram of how the major components fit together during a test run: + +```mermaid +graph TD + subgraph "Orchestration & Testing (Playwright)" + Runner["Playwright Test Runner"] + BrowserContext["Virtual Browser Engine"] + NetworkRouter["Built-in Network Router"] + end + + subgraph "Local Application" + SilentServer["Local Web Server
(Vite booted silently)"] + App["Topola Viewer App
(React / SVG Charts)"] + end + + subgraph "Local Mock Data" + GedFixture["Local Sample File
(family.ged)"] + VirtualIframe["Virtual HTML Page
(Embedded Mode Wrapper)"] + end + + %% Execution connections + Runner -->|1. Launches| SilentServer + Runner -->|2. Controls| BrowserContext + BrowserContext -->|3. Loads App from| SilentServer + SilentServer -->|4. Serves Code to| BrowserContext + + %% Mocking connections + BrowserContext -->|5. Requests Data| NetworkRouter + NetworkRouter -->|6. Redirects to| GedFixture + BrowserContext -->|7. Simulates Embed with| VirtualIframe +``` + +### Breakdown of Major Components + +#### 1. The Playwright Test Runner +The central brain of our testing environment. It handles parsing our TypeScript test specifications, managing test execution, starting the local web server, controlling the virtual browser instances, and generating reports. + +#### 2. The Local Web Server (Vite / Preview) +A local server hosting the Topola Viewer application. To ensure E2E tests validate the exact production bundle that will be deployed, the test runner in CI environments boots the production-built files via Vite's preview server (`npx vite preview --port 3000 --strictPort`), with the build process decoupled and executed earlier in the CI workflow job. Local environments dynamically run Vite dev server (`npx vite --no-open --port 3000 --strictPort`) or reuse an existing running server to maintain fast iteration cycles. + +#### 3. The Built-in Network Router (Interception) +The central network traffic controller. When the application attempts to fetch the family tree, our network router intercepts the request and returns our offline sample data instead. To prevent browser CORS blocks, the mock response explicitly supplies the `Access-Control-Allow-Origin: *` header. Wildcard interceptors match any request ending with `/family.ged`, allowing E2E tests to load family tree data consistently from local mock data without hardcoding public GitHub URL paths or relying on CORS proxy overrides. + +#### 4. The Virtual Page Generator (Iframe Wrapper) +A mechanism for generating temporary webpage wrappers directly in-memory during test execution. To simulate how our app works inside an iframe, Playwright serves a dynamic wrapper page on a mock URL `/test-embedded-frame.html`. This wrapper page loads our app inside an iframe and handles standard bidirectional postMessage handshakes. Because the application uses `HashRouter`, the iframe source points to the hash route `/#/view?embedded=true&handleCors=false` to ensure React Router matches the `/view` route. + +#### 5. The Browser Environment Injector (Init Scripts) +Allows the test runner to pre-configure the browser's environment before the web page's own code executes. This is essential for simulating experimental features like WebMCP tool registration. The injector exposes a mock registration API that pushes registered tools to the browser's global `window.__registeredTools` array. The runner evaluates execution blocks *inside* the browser context to invoke these tools' non-serializable callbacks, asserting tool registration and visual UI updates. + +## 3. Alternatives Considered & Rejected + +To establish clear architectural guardrails and prevent future regressions or redundant work, this section documents the key design alternatives that were evaluated and subsequently ruled out. + +### A. Phased Migration (Coexistence) +* **Considered:** Running both Cypress and Playwright concurrently in the repository and migrating the five test specs one by one over time. +* **Rejected because:** Dual-framework coexistence introduces significant developer friction and technical debt. Developers would have to maintain duplicate configuration files, manage dual dependency pools in `package.json`, and configure complex dual execution stages in GitHub Actions. Given our E2E suite is compact, a "clean break" eliminates Cypress-related dependencies immediately. + +### B. Running E2E Tests Against Live URLs (Real-World Feeds) +* **Considered:** Continuing the Cypress pattern of loading the sample family tree directly from GitHub raw servers over the public internet. +* **Rejected because:** Live URL calls in automated E2E tests are a primary source of test flakiness. Tests can fail randomly due to DNS resolution lags, server downtime, or GitHub API rate-limiting, causing false negatives in our CI pipeline. Using Playwright's network routing to intercept these calls and fulfill them with a local fixture guarantees absolute hermeticity and consistency. + +### C. Maintaining Physical HTML Wrapper Files for Iframe Tests +* **Considered:** Storing a physical `tests/fixtures/embedded_frame.html` file in the repository to act as the container for iframe tests. +* **Rejected because:** Storing a physical, standalone test-only wrapper file can create environment port synchronization issues and static asset mapping overhead. +* **Actual Implementation Note:** The implementation adopted a hybrid approach. A physical template file [embedded_frame.html](file:///home/pwiech/personal/github/topola-viewer/tests/fixtures/embedded_frame.html) is maintained as the structural source of truth for the frame, but it is loaded in-memory and served virtually on `/test-embedded-frame.html` via the network router, keeping it on the same origin/port dynamically to bypass cross-origin iframe blocks. + +### D. Retaining `start-server-and-test` for Dev Server Bootstrapping +* **Considered:** Continuing to rely on `start-server-and-test` or a custom bash script to verify when port `3000` is responsive before running tests. +* **Rejected because:** Playwright's native `webServer` orchestrator is superior, highly optimized, and self-contained. It handles port polling, processes recycling on failures, and performs automatic process cleanups upon exit natively. Keeping `start-server-and-test` adds unnecessary external library dependencies and script complexity. + +### E. Initial Multi-Browser & Multi-Device Targets +* **Considered:** Configuring Chromium, Firefox, WebKit, and mobile emulation from Day One. +* **Rejected because:** The immediate goal is to achieve full, stable parity with the existing single-browser Cypress suite. Adding multiple engines right away increases execution time and CI resource consumption. Multi-browser testing can be easily enabled later by adjusting the configuration in `playwright.config.ts`. + +## 4. Detailed Implementation Plan + +This section defines the granular step-by-step instructions and enumerates **every single file** that will be created, modified, or deleted to ensure a flawless migration, including complete, copy-pasteable code contents for all configurations and test specifications matching the actual implementation. + +### A. Enumeration of Files + +#### 1. Files to [DELETE] + +* **[cypress.config.ts](../cypress.config.ts)** + * *Rationale:* Complete removal of Cypress configurations; no longer needed. +* **[cypress/e2e/chart_view.cy.js](../cypress/e2e/chart_view.cy.js)** + * *Rationale:* Outdated JavaScript test spec. Replaced by type-safe `tests/chart_view.spec.ts`. +* **[cypress/e2e/embedded.cy.js](../cypress/e2e/embedded.cy.js)** + * *Rationale:* Outdated JavaScript test spec. Replaced by type-safe `tests/embedded.spec.ts`. +* **[cypress/e2e/intro.cy.js](../cypress/e2e/intro.cy.js)** + * *Rationale:* Outdated JavaScript test spec. Replaced by type-safe `tests/intro.spec.ts`. +* **[cypress/e2e/search.cy.js](../cypress/e2e/search.cy.js)** + * *Rationale:* Outdated JavaScript test spec. Replaced by type-safe `tests/search.spec.ts`. +* **[cypress/e2e/webmcp.cy.js](../cypress/e2e/webmcp.cy.js)** + * *Rationale:* Outdated JavaScript test spec. Replaced by type-safe `tests/webmcp.spec.ts`. +* **`cypress/e2e/README.md`** + * *Rationale:* Legacy test documentation that is no longer applicable. + +#### 2. Files to [MODIFY] + +* **[package.json](../package.json)** + * *Rationale:* Uninstall `cypress` and `start-server-and-test` devDependencies. Install `@playwright/test` and `@types/node`. Add the silent `"preview": "vite preview"` script, replace the `cy:*` test scripts with Playwright E2E execution targets (`"test:e2e": "playwright test"` and `"test:e2e:ui": "playwright test --ui"`), and update the `"prettier"` and `"lint"` scripts to format and lint both the `src/` and `tests/` directories. +* **[jest.config.ts](../jest.config.ts)** + * *Rationale:* Add the `roots` configuration property to isolate Jest unit testing to the `src/` directory, preventing Jest from scanning or executing Playwright specs inside the `tests/` folder. +* **[.gitignore](../.gitignore)** + * *Rationale:* Ignore locally generated Playwright E2E testing artifacts (`playwright-report/`, `test-results/`, and `.playwright/`) to maintain a clean working tree. +* **[PROJECT_STRUCTURE.md](../PROJECT_STRUCTURE.md)** + * *Rationale:* Replace references to Cypress (`cypress/`, `cypress.config.ts`) with Playwright (`tests/`, `playwright.config.ts`) to keep the repository structural documentation fully accurate. +* **[.github/workflows/node.js.yml](../.github/workflows/node.js.yml)** + * *Rationale:* Replace `npm run cy:start-and-run` with cached Playwright execution. E2E tests are executed across all matrix Node environments to maximize testing coverage and parity across runtimes. +* **[.github/workflows/deploy-gh-pages.yml](../.github/workflows/deploy-gh-pages.yml)** + * *Rationale:* Replace `npm run cy:start-and-run` with cached Playwright execution. Critically, the production build step does NOT use `VITE_GOOGLE_ANALYTICS: "false"` so that Google Analytics is preserved on the deployed live site. Instead, we rely entirely on Playwright's network-level interception to block all tracking requests during E2E testing. +* **[.github/workflows/deploy-wikitree-apps.yml](../.github/workflows/deploy-wikitree-apps.yml)** + * *Rationale:* Replace `npm run cy:start-and-run` with cached Playwright execution, ensuring Google Analytics is NOT stripped from the deployed production bundle. Playwright's network interception handles all traffic isolation safely during E2E tests. + +#### 3. Files to [NEW] + +* **[playwright.config.ts](../playwright.config.ts)** + * *Rationale:* Central configuration for Playwright. Configures parallel execution, defines a single Desktop Chrome (Chromium) project, sets the base URL and locale to `'en-US'`, and manages the local dev or preview servers dynamically on port 3000 with strict port enforcement. +* **`tests/tsconfig.json`** + * *Rationale:* Custom localized compiler configuration for E2E tests that extends the root `tsconfig.json` but includes NodeJS environment types explicitly and isolates global types from Jest, keeping E2E environments perfectly isolated. It sets `"noEmit": true` because E2E tests do not need to output compiled JS files. +* **`tests/global.d.ts`** + * *Rationale:* Custom global type declaration file for E2E tests to safely declare `__registeredTools` on the `Window` interface without TypeScript compiler warnings. Redundant overrides for `Navigator` are omitted because the test suite inherits it from the application's core WebMCP declarations. +* **`tests/helpers.ts`** + * *Rationale:* Shared test utilities to encapsulate wildcard route mocking for family tree fetching (`setupGedcomRoute`) and tracking interception (`blockTracking`). This avoids code duplication across spec files. +* **`tests/fixtures/embedded_frame.html`** + * *Rationale:* Physical template wrapper file defining the iframe and message-passing structure for embedded view E2E verification. +* **`src/datasource/testdata/test.ged`** + * *Rationale:* Instead of checking in a duplicate fixture file, we directly read the existing version-controlled test GEDCOM dataset located at `src/datasource/testdata/test.ged` inside the Playwright interceptors. This eliminates file duplication, reduces repository footprint, and ensures a single source of truth. +* **`tests/intro.spec.ts`** + * *Rationale:* Type-safe TS spec checking landing page layout, menu items, and basic static DOM presence. +* **`tests/chart_view.spec.ts`** + * *Rationale:* Type-safe TS spec checking interactive tree navigation, relying on Playwright's auto-waiting to settle D3 transitions, drawer details panels, and routing interception to block analytics/third-party APIs. +* **`tests/search.spec.ts`** + * *Rationale:* Type-safe TS spec checking the search autocompletion (using robust, user-facing locators like `page.getByPlaceholder('Search for people')` to target the input, and Playwright's auto-waiting to handle search debouncing). +* **`tests/webmcp.spec.ts`** + * *Rationale:* Type-safe TS spec verifying WebMCP tools. Emulates out-of-process tool executions inside `navigator.modelContext` by evaluating execution blocks within the browser context, using polling assertions to avoid React `useEffect` mount race conditions. +* **`tests/embedded.spec.ts`** + * *Rationale:* Type-safe TS spec verifying iframe embedded views using a virtually served template file `tests/fixtures/embedded_frame.html` executing the bidirectional postMessage handshake (`ready` / `gedcom`) matching production. + +### B. Step-by-Step Execution Plan & Complete File Contents + +#### Step 1: Dependency Purge & Clean Break +1. Remove legacy Cypress and server-tester modules from `package.json`: + `npm uninstall cypress start-server-and-test` +2. Delete the `cypress.config.ts` configuration and the recursive `cypress/` folder from disk. +3. Install Playwright Test Framework and Node environment types: + `npm install -D @playwright/test @types/node` +4. Download the local browser binaries required for Playwright execution: + `npx playwright install chromium` +5. Update the `"prettier"` script in `package.json` to cover both `src/` and `tests/`: + `"prettier": "prettier --write \"{src,tests}/**/*.{ts,tsx,json}\""` +6. Modify `jest.config.ts` to isolate Jest testing to the `src/` directory and prevent test scanning collisions: + ```typescript + // Add this property to the exported config object in jest.config.ts + roots: ["/src"], + ``` +7. Update the repository structural documentation [PROJECT_STRUCTURE.md](../PROJECT_STRUCTURE.md) to replace Cypress details with Playwright details. + +#### Step 2: Script Alignment & Silent Configs +1. Update the `package.json` scripts: + * Add `"preview": "vite preview"` to support serving the production build in CI. + * Replace `cy:*` script targets with Playwright E2E commands: + * `"test:e2e": "playwright test"` + * `"test:e2e:ui": "playwright test --ui"` +2. Author `playwright.config.ts` to orchestrate the `webServer` dynamically based on execution context, and poll port `3000`. + +**Key Configuration Details for [playwright.config.ts](file:///home/pwiech/personal/github/topola-viewer/playwright.config.ts):** +* **Test Directory**: Target `./tests` folder. +* **Parallelism & CI Tuning**: Enable fully parallel execution (`fullyParallel: true`), disable `forbidOnly` locally but enforce it in CI, and configure retries (2 in CI, 0 locally). +* **Base Configuration**: Set the `baseURL` to `http://localhost:3000`, force the locale to `'en-US'` to ensure consistent translation keys across all runs, and capture traces on first retry. +* **Browsers**: Set up a single project using standard Chromium devices (`Desktop Chrome`). +* **Orchestrated WebServer**: Configure `webServer` to run the Vite dev server (`npx vite --no-open --port 3000 --strictPort`) locally or Vite preview (`npx vite preview --port 3000 --strictPort`) in CI, targeting port `3000` with strict port enforcement and reusing any existing local server instance only in local mode. + +#### Step 3: Establish Test Directories & Compile Settings +1. Create the folder path `tests/` and its subfolder `tests/fixtures/`. +2. Author `tests/tsconfig.json` to isolate test typings from the main application and Jest unit tests: + * **Inheritance & Target**: Extends the root `tsconfig.json`, setting `module` and `moduleResolution` to `NodeNext` and targeting `ES2022`. + * **Isolated Types**: Excludes Jest/app global typings and explicitly pulls in only the `"node"` types. + * **No Output**: Sets `"noEmit": true` as E2E specs do not need compilation output. +3. Author `tests/global.d.ts` to provide TypeScript type definitions for mocked window objects: + * **Type Extension**: Declares `__registeredTools?` on the global `Window` interface to prevent TypeScript compilation errors during WebMCP mocks. +4. Author `tests/helpers.ts` to provide reusable mock setups and routing interceptions: + * **Tracking Blockers**: Implements a `blockTracking(context)` helper that intercepts and aborts requests targeting Google Analytics and Tag Manager (`**/*google-analytics.com/**`, `**/*googletagmanager.com/**`) to guarantee hermetic and fast test execution. + * **GEDCOM Mocks**: Implements a `setupGedcomRoute(context)` helper that reads the version-controlled local dataset (`src/datasource/testdata/test.ged`) and routes all requests matching `**/family.ged` to be fulfilled with it, serving a `200 OK` response with CORS enablement headers (`Access-Control-Allow-Origin: *`). +5. Author the physical template wrapper file `tests/fixtures/embedded_frame.html` for testing embedded iframe communications: + * **Structure**: Defines a standard wrapper document housing an iframe that points to the app's embedded route: `/#/view?embedded=true&handleCors=false`. + * **Bidirectional Handshake**: Contains script block executing a `fetch()` to get the GEDCOM content. It listens for a `'ready'` postMessage from the child iframe, and posts the raw GEDCOM content back with a `'gedcom'` message once the handshake is complete. + +#### Step 4: Spec Translation & Spec Designs + +##### 1. Intro Test (`tests/intro.spec.ts`) +Checks the landing page layout, menu items, and basic static DOM presence: +* **Setup**: Leverages `beforeEach` to block analytics and tracking servers using the `blockTracking` helper, then loads the index page (`/`). +* **Assertions**: + * Verifies that the main intro landing text content (specifically checking for the presence of `'Examples'`) is visible on the page. + * Asserts that core action buttons in the menu (exact text `'Open file'` and `'Load from URL'`) are properly rendered and visible to the user. + +##### 2. Chart View Test (`tests/chart_view.spec.ts`) +Checks tree navigation, settles transitions, details panels, and routing interception: +* **Setup**: Employs `beforeEach` to configure route intercepts via `setupGedcomRoute` and navigates to the app passing a mock URL parameter (`/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged`). +* **Assertions**: + * Verifies that the viewer loads and renders the chart nodes, asserting that the name `'Bonifacy'` is visible inside the `#content` container. + * Tests chart interactive capabilities by clicking an individual node (e.g., `'Radobod'`) with `{force: true}` to navigate, asserting that a child node (e.g., `'Chike'`) animates and renders. + * Verifies side-drawer details rendering by asserting that person-specific data (e.g., `'a random note'`) is visible. + +##### 3. Search Test (`tests/search.spec.ts`) +Checks search input, suggestion popups, debouncing, and navigation updates: +* **Setup**: Prepares GEDCOM routing via `setupGedcomRoute` and loads the chart view. +* **Assertions**: + * Targets the search input using the user-facing selector `page.getByPlaceholder('Search for people')`. + * Fills in search queries (e.g., `'chik'`), verifies the autocomplete results panel (`.results`) debounces and renders the target suggestion (`'Chike'`). + * Triggers selection by pressing `'Enter'` and asserts that the main chart view successfully shifts focus and renders the target person's node. + +##### 4. WebMCP Test (`tests/webmcp.spec.ts`) +Bridges out-of-process serialization, checks tool registrations, and asserts detail updates: +* **Setup**: Configures GEDCOM intercepts. Prior to navigation, it injects an early script (`page.addInitScript`) that mocks `navigator.modelContext.registerTool` and `navigator.modelContext.unregisterTool` APIs, populating tool metadata in a global `window.__registeredTools` list. +* **Assertions**: + * Verifies all core WebMCP tools (`get_selected_person`, `search_indi`, `inspect_indi`, `focus_indi`, `find_relationship_path`, `get_ancestors`, `get_descendants`) register correctly. Uses a polling assertion (`page.waitForFunction`) to safely wait for the React hook mounting cycle. + * Tests interactive integration by evaluating a script in the browser context that calls the registered `focus_indi` tool callback with target identifier (`{id: 'I21'}`), then asserts that the UI automatically updates the viewer focus to `'Chike'`. + +##### 5. Embedded Test (`tests/embedded.spec.ts`) +Serves local wrapper page template virtually and handles complete bidirectional postMessage handshake: +* **Setup**: Prepares standard GEDCOM routing. Reads the physical `embedded_frame.html` template from disk and registers a route interceptor serving it dynamically at `/test-embedded-frame.html` so both frames stay on the same origin. +* **Assertions**: + * Navigates to `/test-embedded-frame.html`. + * Targets the inner iframe using `page.frameLocator('#topolaFrame')`. + * Verifies that the bidirectional message passing functions correctly by asserting that the inner iframe successfully receives and renders the root family chart nodes (asserting the presence of `'Bonifacy'`). + +#### Step 5: CI Pipeline Alignment + +Modify the GitHub Actions YAML scripts in `.github/workflows/` to replace all legacy Cypress execution steps. E2E testing runs on all Node environments in the pipeline matrix for comprehensive validation. + +##### 1. Key Steps for `node.js.yml` +* **Prettier & Lint Checks**: Run formatting checks across both the application and the new tests using `npx prettier --check "{src,tests}/**/*.{ts,tsx,json}"` and run `npm run lint`. +* **Build & Test**: Run standard `npm run build` and unit tests via `npm test`. +* **E2E Spec Typecheck**: Run `npx tsc -p tests/tsconfig.json --noEmit` to guarantee complete E2E spec compilation/type correctness. +* **Playwright Browser Caching**: Extract the current Playwright version and cache the browser binaries (`~/.cache/ms-playwright`) dynamically using `actions/cache@v4` to prevent redundant downloads. +* **Execution**: Install necessary OS libraries using `npx playwright install-deps chromium`, install the browser binary, and run the test runner with `npm run test:e2e`. +* **Artifact Storage**: Archive the resulting `playwright-report/` directory on failure or completion using `actions/upload-artifact@v4` with a 30-day retention period. + +##### 2. Key Steps for `deploy-gh-pages.yml` & `deploy-wikitree-apps.yml` +* Integrate identical caching, dependency installation, execution (`npm run test:e2e`), and artifact storage steps to gate staging/production deployments. +* Crucially, do **NOT** disable Google Analytics during the app building phase (`VITE_GOOGLE_ANALYTICS` should not be modified), since Playwright's built-in network router handles isolating tracking traffic securely during testing. + +## 5. Future Considerations + +### A. Visual Regression (Screenshot) Testing +While the initial migration focuses on full parity with standard Cypress text-assertion specs, Topola Viewer is a highly visual, SVG-driven charting engine. In a future iteration, we should introduce Playwright visual snapshot/regression testing using `expect(page.locator('#content svg')).toHaveScreenshot('default-chart.png')`. This will automatically catch layout regressions, overlaps, and styled box positioning issues that DOM text assertions cannot detect. + +### B. File Upload Flow Testing +To ensure users can reliably upload their family trees locally, a future iteration should introduce E2E tests for local file ingestion: +* **Single File Upload (.ged)**: Simulating selection and upload of `.ged` text files using Playwright's `setInputFiles()` or `filechooser` events, verifying that the application correctly transitions to the visual chart view. +* **Multi-File / Image Upload**: Verifying that uploading a `.ged` file along with associated `.jpg` or `.png` image assets resolves and binds photos to individuals in the chart without errors. +* **Fixture Management**: This will require checking in small, 1x1 pixel dummy images to act as local mock visual resources. + +### C. CI/CD Pipeline Optimization (Shared E2E Gating Job) +Currently, the manual deployment workflow `deploy-everywhere.yml` triggers both `deploy-gh-pages.yml` and `deploy-wikitree-apps.yml` in parallel, which duplicates E2E test execution and results in running the slow Playwright test runner twice simultaneously. A future optimization should restructure the workflows to execute the E2E validation once as a gating pre-deploy job (`needs: e2e-validation`), or rely strictly on the main merge-to-master CI pipeline validation, ensuring manual deployments are fast, clean, and lightweight. diff --git a/jest.config.ts b/jest.config.ts index 7d9e0fd..b671ec1 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,6 +1,7 @@ import type { Config } from '@jest/types'; const config: Config.InitialOptions = { + roots: ["/src"], testEnvironment: "node", transform: { "^.+.tsx?$": ["ts-jest", {}], diff --git a/package-lock.json b/package-lock.json index 643427c..694b53d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "@formatjs/fast-memoize": "^2.2.6", "@formatjs/intl": "^2.10.15", "@jest/globals": "^29.7.0", + "@playwright/test": "^1.59.1", "@types/adm-zip": "^0.5.0", "@types/array.prototype.flatmap": "^1.2.2", "@types/d3-array": "^3.2.1", @@ -65,12 +66,12 @@ "@types/js-cookie": "^3.0.6", "@types/lunr": "^2.3.3", "@types/md5": "^2.3.0", + "@types/node": "^25.6.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "@vitejs/plugin-react": "^4.3.4", - "cypress": "^13.17.0", "eslint": "^7.32.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.1.1", @@ -80,7 +81,6 @@ "prettier": "^3.8.3", "prettier-plugin-organize-imports": "^4.1.0", "run-script-os": "^1.1.6", - "start-server-and-test": "^2.0.9", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript": "^5.7.3", @@ -758,15 +758,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "dev": true, @@ -892,51 +883,6 @@ "node": ">=18" } }, - "node_modules/@cypress/request": { - "version": "3.0.10", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~4.0.4", - "http-signature": "~1.4.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "performance-now": "^2.1.0", - "qs": "~6.14.1", - "safe-buffer": "^5.1.2", - "tough-cookie": "^5.0.0", - "tunnel-agent": "^0.6.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@cypress/xvfb": { - "version": "1.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.1.0", - "lodash.once": "^4.1.1" - } - }, - "node_modules/@cypress/xvfb/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", @@ -1575,19 +1521,6 @@ "tslib": "2" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.5.0", "dev": true, @@ -2027,6 +1960,22 @@ "node": ">= 8" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "license": "MIT", @@ -2393,24 +2342,6 @@ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "dev": true, @@ -2656,11 +2587,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.10", + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~7.19.0" } }, "node_modules/@types/pako": { @@ -2699,16 +2632,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/sizzle": { - "version": "2.3.9", - "dev": true, - "license": "MIT" - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "dev": true, @@ -2736,15 +2659,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", @@ -3070,18 +2984,6 @@ "node": ">= 14" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ajv": { "version": "6.12.6", "dev": true, @@ -3153,30 +3055,6 @@ "node": ">= 8" } }, - "node_modules/arch": { - "version": "2.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/arg": { - "version": "5.0.2", - "dev": true, - "license": "MIT" - }, "node_modules/argparse": { "version": "1.0.10", "dev": true, @@ -3329,22 +3207,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asn1": { - "version": "0.2.6", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/astral-regex": { "version": "2.0.0", "dev": true, @@ -3369,14 +3231,6 @@ "version": "0.4.0", "license": "MIT" }, - "node_modules/at-least-node": { - "version": "1.0.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "license": "MIT", @@ -3390,41 +3244,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.13.2", - "dev": true, - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", - "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^2.1.0" - } - }, - "node_modules/axios/node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "dev": true, @@ -3568,24 +3387,6 @@ ], "license": "MIT" }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/blob-util": { - "version": "2.0.2", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/bluebird": { - "version": "3.7.2", - "dev": true, - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "1.1.11", "dev": true, @@ -3678,26 +3479,10 @@ "ieee754": "^1.2.1" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "license": "MIT" }, - "node_modules/cachedir": { - "version": "2.4.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -3807,11 +3592,6 @@ "license": "MIT", "optional": true }, - "node_modules/caseless": { - "version": "0.12.0", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/ccount": { "version": "2.0.1", "license": "MIT", @@ -3885,81 +3665,11 @@ "node": "*" } }, - "node_modules/check-more-types": { - "version": "2.24.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ci-info": { - "version": "4.1.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/cjs-module-lexer": { "version": "1.4.3", "dev": true, "license": "MIT" }, - "node_modules/clean-stack": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-table3": { - "version": "0.6.5", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cliui": { "version": "8.0.1", "dev": true, @@ -4010,11 +3720,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colorette": { - "version": "2.0.20", - "dev": true, - "license": "MIT" - }, "node_modules/combined-stream": { "version": "1.0.8", "license": "MIT", @@ -4033,22 +3738,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/commander": { - "version": "6.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/common-tags": { - "version": "1.8.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/commondir": { "version": "1.0.1", "dev": true, @@ -4094,11 +3783,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/core-util-is": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, "node_modules/create-jest": { "version": "29.7.0", "dev": true, @@ -4175,97 +3859,6 @@ "version": "3.1.3", "license": "MIT" }, - "node_modules/cypress": { - "version": "13.17.0", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@cypress/request": "^3.0.6", - "@cypress/xvfb": "^1.2.4", - "@types/sinonjs__fake-timers": "8.1.1", - "@types/sizzle": "^2.3.2", - "arch": "^2.2.0", - "blob-util": "^2.0.2", - "bluebird": "^3.7.2", - "buffer": "^5.7.1", - "cachedir": "^2.3.0", - "chalk": "^4.1.0", - "check-more-types": "^2.24.0", - "ci-info": "^4.0.0", - "cli-cursor": "^3.1.0", - "cli-table3": "~0.6.1", - "commander": "^6.2.1", - "common-tags": "^1.8.0", - "dayjs": "^1.10.4", - "debug": "^4.3.4", - "enquirer": "^2.3.6", - "eventemitter2": "6.4.7", - "execa": "4.1.0", - "executable": "^4.1.1", - "extract-zip": "2.0.1", - "figures": "^3.2.0", - "fs-extra": "^9.1.0", - "getos": "^3.2.1", - "is-installed-globally": "~0.4.0", - "lazy-ass": "^1.6.0", - "listr2": "^3.8.3", - "lodash": "^4.17.21", - "log-symbols": "^4.0.0", - "minimist": "^1.2.8", - "ospath": "^1.2.2", - "pretty-bytes": "^5.6.0", - "process": "^0.11.10", - "proxy-from-env": "1.0.0", - "request-progress": "^3.0.0", - "semver": "^7.5.3", - "supports-color": "^8.1.1", - "tmp": "~0.2.3", - "tree-kill": "1.2.2", - "untildify": "^4.0.0", - "yauzl": "^2.10.0" - }, - "bin": { - "cypress": "bin/cypress" - }, - "engines": { - "node": "^16.0.0 || ^18.0.0 || >=20.0.0" - } - }, - "node_modules/cypress/node_modules/buffer": { - "version": "5.7.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/cypress/node_modules/semver": { - "version": "7.6.3", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/d3": { "version": "6.7.0", "license": "BSD-3-Clause", @@ -4751,17 +4344,6 @@ "version": "1.0.1", "license": "ISC" }, - "node_modules/dashdash": { - "version": "1.14.1", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/data-urls": { "version": "5.0.0", "dev": true, @@ -4819,11 +4401,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dayjs": { - "version": "1.11.13", - "dev": true, - "license": "MIT" - }, "node_modules/debounce": { "version": "2.2.0", "license": "MIT", @@ -5029,20 +4606,6 @@ "node": ">= 0.4" } }, - "node_modules/duplexer": { - "version": "0.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/ejs": { "version": "3.1.10", "dev": true, @@ -5083,14 +4646,6 @@ "dev": true, "license": "MIT" }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/enquirer": { "version": "2.4.1", "dev": true, @@ -5672,58 +5227,6 @@ "node": ">=0.10.0" } }, - "node_modules/event-stream": { - "version": "3.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - } - }, - "node_modules/eventemitter2": { - "version": "6.4.7", - "dev": true, - "license": "MIT" - }, - "node_modules/execa": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/executable": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.2.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/exenv": { "version": "1.2.2", "license": "BSD-3-Clause" @@ -5762,33 +5265,6 @@ "version": "3.0.2", "license": "MIT" }, - "node_modules/extract-zip": { - "version": "2.0.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, "node_modules/family-chart": { "version": "0.2.1", "license": "ISC", @@ -5866,40 +5342,10 @@ "bser": "2.1.1" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fflate": { "version": "0.8.2", "license": "MIT" }, - "node_modules/figures": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "dev": true, @@ -6033,27 +5479,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -6069,14 +5494,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -6093,25 +5510,6 @@ "node": ">= 6" } }, - "node_modules/from": { - "version": "0.1.7", - "dev": true, - "license": "MIT" - }, - "node_modules/fs-extra": { - "version": "9.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "dev": true, @@ -6228,20 +5626,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", "license": "MIT", @@ -6257,22 +5641,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/getos": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "async": "^3.2.0" - } - }, - "node_modules/getpass": { - "version": "0.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/gh-pages": { "version": "6.3.0", "dev": true, @@ -6345,20 +5713,6 @@ "node": ">= 6" } }, - "node_modules/global-dirs": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/globals": { "version": "11.12.0", "dev": true, @@ -6609,19 +5963,6 @@ "node": ">= 14" } }, - "node_modules/http-signature": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^2.0.2", - "sshpk": "^1.18.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/https-proxy-agent": { "version": "7.0.6", "dev": true, @@ -6634,14 +5975,6 @@ "node": ">= 14" } }, - "node_modules/human-signals": { - "version": "1.1.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8.12.0" - } - }, "node_modules/iconv-lite": { "version": "0.4.24", "license": "MIT", @@ -6719,14 +6052,6 @@ "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/inflight": { "version": "1.0.6", "dev": true, @@ -6740,14 +6065,6 @@ "version": "2.0.4", "license": "ISC" }, - "node_modules/ini": { - "version": "2.0.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/internal-slot": { "version": "1.1.0", "license": "MIT", @@ -6973,21 +6290,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-installed-globally": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-map": { "version": "2.0.3", "license": "MIT", @@ -7032,14 +6334,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "license": "MIT", @@ -7147,22 +6441,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-weakmap": { "version": "2.0.2", "license": "MIT", @@ -7211,11 +6489,6 @@ "dev": true, "license": "ISC" }, - "node_modules/isstream": { - "version": "0.1.2", - "dev": true, - "license": "MIT" - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "dev": true, @@ -8331,18 +7604,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/joi": { - "version": "17.13.3", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, "node_modules/jquery": { "version": "3.7.1", "license": "MIT" @@ -8370,11 +7631,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "0.1.1", - "dev": true, - "license": "MIT" - }, "node_modules/jsdom": { "version": "26.0.0", "dev": true, @@ -8435,11 +7691,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema": { - "version": "0.4.0", - "dev": true, - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "dev": true, @@ -8450,11 +7701,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "dev": true, - "license": "ISC" - }, "node_modules/json5": { "version": "2.2.3", "dev": true, @@ -8492,20 +7738,6 @@ "html2canvas": "^1.0.0-rc.5" } }, - "node_modules/jsprim": { - "version": "2.0.2", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -8542,14 +7774,6 @@ "node": ">=6" } }, - "node_modules/lazy-ass": { - "version": "1.6.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "> 0.8" - } - }, "node_modules/leven": { "version": "3.1.0", "dev": true, @@ -8582,32 +7806,6 @@ "uc.micro": "^1.0.1" } }, - "node_modules/listr2": { - "version": "3.14.0", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.16", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.5.1", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } - } - }, "node_modules/locate-path": { "version": "5.0.0", "dev": true, @@ -8637,77 +7835,11 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.once": { - "version": "4.1.1", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.truncate": { "version": "4.4.2", "dev": true, "license": "MIT" }, - "node_modules/log-symbols": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "6.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "license": "MIT", @@ -8761,10 +7893,6 @@ "tmpl": "1.0.5" } }, - "node_modules/map-stream": { - "version": "0.1.0", - "dev": true - }, "node_modules/math-intrinsics": { "version": "1.1.0", "license": "MIT", @@ -9294,14 +8422,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/ms": { "version": "2.1.3", "license": "MIT" @@ -9562,11 +8682,6 @@ "version": "1.2.0", "license": "Unlicense" }, - "node_modules/ospath": { - "version": "1.2.2", - "dev": true, - "license": "MIT" - }, "node_modules/own-keys": { "version": "1.0.1", "license": "MIT", @@ -9607,20 +8722,6 @@ "node": ">=8" } }, - "node_modules/p-map": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-try": { "version": "2.2.0", "dev": true, @@ -9721,26 +8822,10 @@ "node": ">=8" } }, - "node_modules/pause-stream": { - "version": "0.0.11", - "dev": true, - "license": [ - "MIT", - "Apache2" - ], - "dependencies": { - "through": "~2.3" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "dev": true, - "license": "MIT" - }, "node_modules/performance-now": { "version": "2.1.0", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/picocolors": { "version": "1.1.1", @@ -9758,14 +8843,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pirates": { "version": "4.0.6", "dev": true, @@ -9785,6 +8862,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "license": "MIT", @@ -9858,17 +8982,6 @@ } } }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/pretty-format": { "version": "26.6.2", "dev": true, @@ -9906,14 +9019,6 @@ "@types/yargs-parser": "*" } }, - "node_modules/process": { - "version": "0.11.10", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/progress": { "version": "2.0.3", "dev": true, @@ -9955,34 +9060,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/proxy-from-env": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ps-tree": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "event-stream": "=3.3.4" - }, - "bin": { - "ps-tree": "bin/ps-tree.js" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "dev": true, @@ -10006,20 +9083,6 @@ ], "license": "MIT" }, - "node_modules/qs": { - "version": "6.14.2", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/query-string": { "version": "9.1.1", "license": "MIT", @@ -10260,14 +9323,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/request-progress": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "throttleit": "^1.0.0" - } - }, "node_modules/require-directory": { "version": "2.1.1", "dev": true, @@ -10338,18 +9393,6 @@ "node": ">=10" } }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/reusify": { "version": "1.0.4", "dev": true, @@ -10359,11 +9402,6 @@ "node": ">=0.10.0" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "dev": true, - "license": "MIT" - }, "node_modules/rgbcolor": { "version": "1.0.1", "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", @@ -10469,14 +9507,6 @@ "version": "1.3.3", "license": "BSD-3-Clause" }, - "node_modules/rxjs": { - "version": "7.8.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/safe-array-concat": { "version": "1.1.3", "license": "MIT", @@ -10750,19 +9780,6 @@ "node": ">=8" } }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/source-map": { "version": "0.6.1", "dev": true, @@ -10787,17 +9804,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/split": { - "version": "0.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "through": "2" - }, - "engines": { - "node": "*" - } - }, "node_modules/split-on-first": { "version": "3.0.0", "license": "MIT", @@ -10813,30 +9819,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/sshpk": { - "version": "1.18.0", - "dev": true, - "license": "MIT", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stack-utils": { "version": "2.0.6", "dev": true, @@ -10864,70 +9846,6 @@ "node": ">=0.1.14" } }, - "node_modules/start-server-and-test": { - "version": "2.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "arg": "^5.0.2", - "bluebird": "3.7.2", - "check-more-types": "2.24.0", - "debug": "4.4.0", - "execa": "5.1.1", - "lazy-ass": "1.6.0", - "ps-tree": "1.2.0", - "wait-on": "8.0.2" - }, - "bin": { - "server-test": "src/bin/start.js", - "start-server-and-test": "src/bin/start.js", - "start-test": "src/bin/start.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/start-server-and-test/node_modules/execa": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/start-server-and-test/node_modules/get-stream": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/start-server-and-test/node_modules/human-signals": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -10941,14 +9859,6 @@ "node": ">= 0.4" } }, - "node_modules/stream-combiner": { - "version": "0.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexer": "~0.1.1" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "license": "MIT", @@ -11254,19 +10164,6 @@ "dev": true, "license": "MIT" }, - "node_modules/throttleit": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/through": { - "version": "2.3.8", - "dev": true, - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.14", "dev": true, @@ -11329,14 +10226,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tmp": { - "version": "0.2.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, "node_modules/tmpl": { "version": "1.0.5", "dev": true, @@ -11402,14 +10291,6 @@ "node": ">=5" } }, - "node_modules/tree-kill": { - "version": "1.2.2", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, "node_modules/trim-lines": { "version": "3.0.1", "license": "MIT", @@ -11588,26 +10469,10 @@ "dev": true, "license": "0BSD" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/turbocommons-ts": { "version": "3.12.0", "license": "Apache-2.0" }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "dev": true, - "license": "Unlicense" - }, "node_modules/type-check": { "version": "0.4.0", "dev": true, @@ -11741,7 +10606,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "dev": true, "license": "MIT" }, @@ -11828,14 +10695,6 @@ "node": ">= 10.0.0" } }, - "node_modules/untildify": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/update-browserslist-db": { "version": "1.1.2", "dev": true, @@ -11885,14 +10744,6 @@ "base64-arraybuffer": "^1.0.2" } }, - "node_modules/uuid": { - "version": "8.3.2", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/v8-compile-cache": { "version": "2.4.0", "dev": true, @@ -11916,19 +10767,6 @@ "node": ">=10.12.0" } }, - "node_modules/verror": { - "version": "1.10.0", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "node_modules/vfile": { "version": "6.0.3", "license": "MIT", @@ -12100,24 +10938,6 @@ "node": ">=18" } }, - "node_modules/wait-on": { - "version": "8.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "axios": "^1.7.9", - "joi": "^17.13.3", - "lodash": "^4.17.21", - "minimist": "^1.2.8", - "rxjs": "^7.8.1" - }, - "bin": { - "wait-on": "bin/wait-on" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/walker": { "version": "1.0.8", "dev": true, @@ -12397,15 +11217,6 @@ "node": ">=12" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yn": { "version": "3.1.1", "dev": true, diff --git a/package.json b/package.json index 50b74ce..66621af 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@formatjs/fast-memoize": "^2.2.6", "@formatjs/intl": "^2.10.15", "@jest/globals": "^29.7.0", + "@playwright/test": "^1.59.1", "@types/adm-zip": "^0.5.0", "@types/array.prototype.flatmap": "^1.2.2", "@types/d3-array": "^3.2.1", @@ -60,12 +61,12 @@ "@types/js-cookie": "^3.0.6", "@types/lunr": "^2.3.3", "@types/md5": "^2.3.0", + "@types/node": "^25.6.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "@vitejs/plugin-react": "^4.3.4", - "cypress": "^13.17.0", "eslint": "^7.32.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.1.1", @@ -75,7 +76,6 @@ "prettier": "^3.8.3", "prettier-plugin-organize-imports": "^4.1.0", "run-script-os": "^1.1.6", - "start-server-and-test": "^2.0.9", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript": "^5.7.3", @@ -86,16 +86,15 @@ "start": "vite", "build": "tsc && vite build", "test": "jest", - "prettier": "prettier --write \"src/**/*.{ts,tsx,json}\"", - "lint": "eslint src --ext .ts,.tsx", + "prettier": "prettier --write \"{src,tests}/**/*.{ts,tsx,json}\"", + "lint": "eslint src tests --ext .ts,.tsx", "predeploy": "npm run build", "deploy": "gh-pages -d dist", "predeploy-wikitree": "npm run build", "deploy-wikitree": "./deploy-wikitree.sh", - "cy:run": "cypress run", - "cy:start-and-run": "run-script-os", - "cy:start-and-run:default": "BROWSER=none start-server-and-test start localhost:3000 cy:run", - "cy:start-and-run:windows": "set BROWSER=none && start-server-and-test start localhost:3000 cy:run" + "preview": "vite preview", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui" }, "homepage": ".", "browserslist": [ diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..3d735bf --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,32 @@ +import {defineConfig, devices} from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? '100%' : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:3000', + locale: 'en-US', // Forces consistent translation keys across locales for robust placeholder selectors + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], + webServer: { + command: process.env.CI + ? 'npx vite preview --port 3000 --strictPort' // CI already builds the app; preview directly with strict port + : 'npx vite --no-open --port 3000 --strictPort', + port: 3000, + reuseExistingServer: !process.env.CI, + stdout: 'ignore', + stderr: 'pipe', + }, +}); diff --git a/screenshot.png b/screenshot.png deleted file mode 100644 index 624268bedca5a50811f977c028b353ac600e6664..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 87318 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 { onSelection({id, generation: 0}); }); - }, [mcpBridge]); + }, [mcpBridge, location]); function updateUrl(args: queryString.ParsedQuery) { const search = queryString.parse(location.search); diff --git a/tests/chart_view.spec.ts b/tests/chart_view.spec.ts new file mode 100644 index 0000000..aff9f9a --- /dev/null +++ b/tests/chart_view.spec.ts @@ -0,0 +1,26 @@ +import {expect, test} from '@playwright/test'; +import {setupGedcomRoute} from './helpers'; + +test.describe('Chart view', () => { + test.beforeEach(async ({page, context}) => { + await setupGedcomRoute(context); + await page.goto('/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged'); + }); + + test('loads data from URL', async ({page}) => { + await expect(page.locator('#content')).toContainText('Bonifacy'); + }); + + test('Animates chart', async ({page}) => { + await expect(page.locator('#content')).not.toContainText('Chike'); + + // Click Radobod's node. force: true is required because D3 wraps the text in a border + // that intercepts pointer events, which is expected SVG chart layout behavior. + await page.getByText('Radobod').click({force: true}); + await expect(page.locator('#content')).toContainText('Chike'); + }); + + test('shows the right panel', async ({page}) => { + await expect(page.locator('#content')).toContainText('a random note'); + }); +}); diff --git a/tests/embedded.spec.ts b/tests/embedded.spec.ts new file mode 100644 index 0000000..09d896c --- /dev/null +++ b/tests/embedded.spec.ts @@ -0,0 +1,33 @@ +import {expect, test} from '@playwright/test'; +import * as fs from 'fs'; +import {setupGedcomRoute} from './helpers'; + +test.describe('Embedded mode', () => { + test('shows data', async ({page, context}) => { + // Intercept family.ged requests coming from parent frame. + await setupGedcomRoute(context); + + // Read the physical HTML wrapper template file. + const wrapperHtml = fs.readFileSync( + 'tests/fixtures/embedded_frame.html', + 'utf-8', + ); + + // Route parent page wrapper virtually on the same origin/port dynamically. + await context.route(`**/test-embedded-frame.html`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'text/html', + body: wrapperHtml, + }); + }); + + // Load the virtual wrapper page. + await page.goto(`/test-embedded-frame.html`); + + // Assert child iframe successfully loaded Bonifacy Gibbs. + const iframe = page.frameLocator('#topolaFrame'); + + await expect(iframe.locator('#root')).toContainText('Bonifacy'); + }); +}); diff --git a/tests/fixtures/embedded_frame.html b/tests/fixtures/embedded_frame.html new file mode 100644 index 0000000..fe74920 --- /dev/null +++ b/tests/fixtures/embedded_frame.html @@ -0,0 +1,44 @@ + + + + Embedded Frame Test Wrapper + + + + + + diff --git a/tests/global.d.ts b/tests/global.d.ts new file mode 100644 index 0000000..c6eef78 --- /dev/null +++ b/tests/global.d.ts @@ -0,0 +1,3 @@ +interface Window { + __registeredTools?: any[]; +} diff --git a/tests/helpers.ts b/tests/helpers.ts new file mode 100644 index 0000000..6838292 --- /dev/null +++ b/tests/helpers.ts @@ -0,0 +1,33 @@ +import {BrowserContext} from '@playwright/test'; +import * as fs from 'fs'; + +/** + * Blocks external tracking services (like Google Analytics and Google Tag Manager) + * to guarantee a hermetic and fast test environment. + */ +export async function blockTracking(context: BrowserContext): Promise { + await context.route('**/*google-analytics.com/**', (route) => route.abort()); + await context.route('**/*googletagmanager.com/**', (route) => route.abort()); +} + +/** + * Sets up interception for raw GEDCOM requests, fulfills them with cached test data + * and sets up CORS proxy checks and analytics blocking. + */ +export async function setupGedcomRoute(context: BrowserContext): Promise { + const gedcomContent = fs.readFileSync( + 'src/datasource/testdata/test.ged', + 'utf-8', + ); + + await context.route('**/family.ged', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'text/plain', + headers: {'Access-Control-Allow-Origin': '*'}, + body: gedcomContent, + }); + }); + + await blockTracking(context); +} diff --git a/tests/intro.spec.ts b/tests/intro.spec.ts new file mode 100644 index 0000000..39a5f24 --- /dev/null +++ b/tests/intro.spec.ts @@ -0,0 +1,18 @@ +import {expect, test} from '@playwright/test'; +import {blockTracking} from './helpers'; + +test.describe('Intro page', () => { + test.beforeEach(async ({page, context}) => { + await blockTracking(context); + await page.goto('/'); + }); + + test('displays intro text', async ({page}) => { + await expect(page.getByText('Examples')).toBeVisible(); + }); + + test('displays menu', async ({page}) => { + await expect(page.getByText('Open file', {exact: true})).toBeVisible(); + await expect(page.getByText('Load from URL', {exact: true})).toBeVisible(); + }); +}); diff --git a/tests/search.spec.ts b/tests/search.spec.ts new file mode 100644 index 0000000..74ec2fb --- /dev/null +++ b/tests/search.spec.ts @@ -0,0 +1,22 @@ +import {expect, test} from '@playwright/test'; +import {setupGedcomRoute} from './helpers'; + +test.describe('Search functionality', () => { + test.beforeEach(async ({context}) => { + await setupGedcomRoute(context); + }); + + test('Search works', async ({page}) => { + await page.goto('/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged'); + await expect(page.locator('#content')).not.toContainText('Chike'); + + const searchInput = page.getByPlaceholder('Search for people'); + await searchInput.fill('chik'); + + // Wait for the debounced suggestion panel to render. + await expect(page.locator('.results')).toContainText('Chike'); + + await searchInput.press('Enter'); + await expect(page.locator('#content')).toContainText('Chike'); + }); +}); diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 0000000..f8ed1e6 --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "NodeNext", + "target": "ES2022", + "moduleResolution": "NodeNext", + "types": ["node"], + "noEmit": true + }, + "include": ["./**/*.ts", "./**/*.d.ts"] +} diff --git a/tests/webmcp.spec.ts b/tests/webmcp.spec.ts new file mode 100644 index 0000000..e66affe --- /dev/null +++ b/tests/webmcp.spec.ts @@ -0,0 +1,73 @@ +import {expect, test} from '@playwright/test'; +import {setupGedcomRoute} from './helpers'; + +const EXPECTED_TOOL_NAMES = [ + 'get_selected_person', + 'search_indi', + 'inspect_indi', + 'focus_indi', + 'find_relationship_path', + 'get_ancestors', + 'get_descendants', +]; + +test.describe('WebMCP Integration', () => { + test.beforeEach(async ({page, context}) => { + await setupGedcomRoute(context); + + // Add init script to expose modelContext mock BEFORE application boots. + await page.addInitScript(() => { + const registeredTools: any[] = []; + window.__registeredTools = registeredTools; + window.navigator.modelContext = { + registerTool: (tool: any) => { + registeredTools.push(tool); + }, + unregisterTool: (name: string) => { + const idx = registeredTools.findIndex((t) => t.name === name); + if (idx !== -1) registeredTools.splice(idx, 1); + }, + }; + }); + }); + + test('registers tools to standard modelContext', async ({page}) => { + await page.goto('/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged'); + + // Polling assertion to avoid React useEffect registration race condition. + await page.waitForFunction( + (expectedCount) => window.__registeredTools?.length === expectedCount, + EXPECTED_TOOL_NAMES.length, + ); + + const toolNames = await page.evaluate(() => + window.__registeredTools + ? window.__registeredTools.map((t: any) => t.name) + : [], + ); + expect(toolNames.sort()).toEqual([...EXPECTED_TOOL_NAMES].sort()); + }); + + test('allows running focus_indi tool', async ({page}) => { + await page.goto('/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged'); + await page.waitForFunction( + (expectedCount) => window.__registeredTools?.length === expectedCount, + EXPECTED_TOOL_NAMES.length, + ); + + await expect(page.locator('#content')).toContainText('Radobod'); + await expect(page.locator('#content')).not.toContainText('Chike'); + + // Execute the non-serializable callback inside the browser environment. + await page.evaluate(async () => { + const focusTool = window.__registeredTools + ? window.__registeredTools.find((t: any) => t.name === 'focus_indi') + : null; + if (!focusTool) throw new Error('focus_indi tool not found'); + await focusTool.execute({id: 'I21'}); // Shifts view focus to Chike. + }); + + // Verify that the UI updated automatically in response to the tool action. + await expect(page.locator('#content')).toContainText('Chike'); + }); +});