diff --git a/.github/workflows/prober-docker.yml b/.github/workflows/prober-docker.yml index 559a33b..a98ab5f 100644 --- a/.github/workflows/prober-docker.yml +++ b/.github/workflows/prober-docker.yml @@ -26,6 +26,12 @@ jobs: - name: Pull Docker image run: docker pull ghcr.io/pewu/topola-viewer:latest + - name: Record image digest + run: | + DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/pewu/topola-viewer:latest) + echo "Testing Docker image: $DIGEST" + echo "::notice title=Docker Prober::Testing image $DIGEST" + - name: Run container run: | docker run -d -p 8080:8080 -e STATIC_URL=test.ged \ @@ -33,14 +39,6 @@ jobs: --name topola-prober-${{ github.run_id }} \ ghcr.io/pewu/topola-viewer:latest - - name: Wait for container to be ready - run: | - for i in $(seq 1 30); do - if curl -sf -o /dev/null http://localhost:8080/; then break; fi - sleep 1 - done - curl -sf -o /dev/null http://localhost:8080/ - - name: Use Node.js uses: actions/setup-node@v4 with: @@ -67,6 +65,14 @@ jobs: if: steps.cache-playwright.outputs.cache-hit != 'true' run: npx playwright install chromium + - name: Wait for container to be ready + run: | + for i in $(seq 1 30); do + if curl -sf -o /dev/null http://localhost:8080/; then break; fi + sleep 1 + done + curl -sf -o /dev/null http://localhost:8080/ + - name: Run prober env: PLAYWRIGHT_HTML_REPORT: playwright-report/prober diff --git a/docs/PROBERS_DESIGN.md b/docs/PROBERS_DESIGN.md index bcce035..9102741 100644 --- a/docs/PROBERS_DESIGN.md +++ b/docs/PROBERS_DESIGN.md @@ -258,7 +258,8 @@ Rationale for key configuration decisions: need for separate e2e/visual projects. All prober specs are smoke tests. The project must explicitly use `devices['Desktop Chrome']` to ensure a desktop viewport, because the side panel visibility depends on - `window.matchMedia('(max-width: 767px)')` (see `src/util/url_args.ts:177`). + `window.matchMedia('(max-width: 767px)')` (see the `getShowSidePanel` function in + `src/util/url_args.ts`). Without an explicit device, Playwright's default viewport may be too narrow, causing the side panel to be hidden and the `.details` assertion to fail. @@ -290,7 +291,7 @@ targets a different URL and asserts a different expected name. deployment. * **Note:** Does not use `standalone=true` in the URL. The app defaults to standalone mode when not embedded and no static URL is set (see - `src/util/url_args.ts:198`). + `src/util/url_args.ts`). **Create:** `tests/probers/gh-pages-gedcom.spec.ts` @@ -310,12 +311,12 @@ targets a different URL and asserts a different expected name. * **Expected name:** `Bonifacy` (same as above). * **What it exercises:** WikiTree deployment, CORS proxy from the WikiTree domain. Even on `apps.wikitree.com`, GEDCOM-from-URL uses the CORS proxy - by default (the `handleCors` hostname check in `src/datasource/wikitree_api.ts:273` + by default (the `handleCors` hostname check in `src/datasource/wikitree_api.ts` only affects WikiTree API calls, not GEDCOM URL fetches in - `src/datasource/load_data.ts:174`). Note: probers do not block Google Analytics scripts, - so live-URL prober runs generate real analytics events on each run. This - is intentional — the prober tests the unmodified deployed app, and - blocking analytics would not reflect the real user experience. + `src/datasource/load_data.ts`). Note: probers do not block Google Analytics + scripts, so live-URL prober runs generate real analytics events on each + run. This is intentional — the prober tests the unmodified deployed app, + and blocking analytics would not reflect the real user experience. **Create:** `tests/probers/docker.spec.ts` @@ -339,34 +340,39 @@ targets a different URL and asserts a different expected name. Note: the Docker prober tests the image published to GHCR by `deploy-docker.yml`, ensuring the published artifact is functional. -**Shared test structure** (in each spec): +**Shared test structure** (in `tests/probers/helpers.ts`, called by each spec): ``` -1. Navigate to the target URL. -2. Wait for #content to be visible. This indicates the app has reached - `SHOWING_CHART` state and the React tree has rendered the chart +1. Register browser diagnostics listeners (console errors/warnings, page + errors, failed network requests) that print to stdout for debugging + live-URL failures. +2. Navigate to the target URL with waitUntil: 'domcontentloaded' to avoid + waiting for analytics scripts. +3. Wait for data-testid="content" to be visible. This indicates the app has + reached `SHOWING_CHART` state and the React tree has rendered the chart container. Note: `#content` becomes visible *before* the D3 chart SVG is populated — the actual chart text is rendered by a `useEffect` in the `Chart` component that fires after `#content` appears. The subsequent `#chart` text assertion relies on Playwright's auto-wait to bridge this gap. -3. Assert expected name appears in #chart (chart SVG text). -4. Assert expected name appears in div.details (side panel). -5. Assert .ui.error.message is not visible (no fatal error). -6. Assert .ui.errorPopup.message is not visible (no popup error). +4. Assert expected name appears in data-testid="chart" (chart SVG text). +5. Assert expected name appears in data-testid="details" (side panel). +6. Assert data-testid="error-message" is not visible (no fatal error). +7. Assert data-testid="error-popup" is not visible (no popup error). ``` -The `.ui.errorPopup.message` selector must be scoped at the document level -(e.g., `page.locator('.ui.errorPopup.message')`), not scoped to `#content`, -because `ErrorPopup` uses Semantic UI React's `` which renders at +The `data-testid="error-popup"` selector works at the document level because +Playwright's `getByTestId` searches the entire document, so it matches the +`ErrorPopup` content rendered by Semantic UI React's `` at `document.body` level. Selectors are derived from the source code: * `#content` — main container, visible when chart state is `SHOWING_CHART` - (see `src/pages/view_page.tsx:202`). -* `#chart` — SVG group inside the chart (see `src/chart.tsx:599`). -* `div.details` — side panel Details tab content (see `src/sidepanel/details/details.tsx:357`). + (see the `renderMainArea` function in `src/pages/view_page.tsx`). +* `#chart` — SVG group inside the chart (see `src/chart.tsx`). +* `div.details` — side panel Details tab content (see the `Details` component in + `src/sidepanel/details/details.tsx`). * `.ui.error.message` — fatal error replacing the chart (see `src/components/error_display.tsx`, rendered when state is `ERROR`). The `ui` and `message` classes are added by Semantic UI React's `` @@ -385,9 +391,18 @@ Selectors are derived from the source code: The side panel is expanded by default on desktop viewports (the prober project uses `devices['Desktop Chrome']`). The `getShowSidePanel` function in -`src/util/url_args.ts:177` returns `true` on non-mobile screens, so the `div.details` +`src/util/url_args.ts` returns `true` on non-mobile screens, so the `div.details` container is visible without any URL parameters. +All prober selectors use `data-testid` attributes (e.g., `data-testid="content"`, +`data-testid="chart"`, `data-testid="details"`, `data-testid="error-message"`, +`data-testid="error-popup"`) rather than CSS classes or element IDs. This makes +selectors resilient to CSS class refactors and Semantic UI React internal +changes. The `data-testid` attributes are added to the source components +alongside existing IDs and classes. A shared helper (`tests/probers/helpers.ts`) +encapsulates the prober flow and selector logic, eliminating duplication across +spec files. + ### Step 3: Prober GitHub Actions workflows All prober workflows should declare minimal permissions for security: @@ -488,13 +503,22 @@ before testing). (Pull the image published by deploy-docker.yml. This tests the actual published artifact, not a local build. The GHCR package is public, so no `docker login` authentication step is required.) -3. Run container: docker run -d -p 8080:8080 -e STATIC_URL=test.ged +3. Record image digest: docker inspect --format='{{index .RepoDigests 0}}' + ghcr.io/pewu/topola-viewer:latest. Prints the full digest (e.g., + ghcr.io/pewu/topola-viewer@sha256:abc123...) to the workflow log and + as a GitHub Actions notice. This provides traceability — if the prober + fails, you can verify which exact image was tested. +4. Run container: docker run -d -p 8080:8080 -e STATIC_URL=test.ged -v $(pwd)/src/datasource/testdata/test.ged:/app/public/test.ged --name topola-prober-${{ github.run_id }} ghcr.io/pewu/topola-viewer:latest (Use a unique container name with github.run_id to prevent name conflicts if a previous run didn't clean up or if runs overlap.) -4. Wait for container to be ready: use a bash retry loop with `curl` to +5. Setup Node.js 24.x with npm cache. +6. Run npm ci. +7. Get Playwright version (same pattern as node.js.yml). +8. Cache and install Playwright (same as live-URL probers). +9. Wait for container to be ready: use a bash retry loop with `curl` to poll `http://localhost:8080/` until it responds with HTTP 200 (timeout 30s, 1s interval): ```bash @@ -506,17 +530,20 @@ before testing). ``` The final `curl` ensures the workflow fails with a clear error if the container never became ready. This prevents a race condition - where the test runs before Caddy is ready to serve requests. -5. Setup Node.js 24.x with npm cache. -6. Run npm ci. -7. Get Playwright version (same pattern as node.js.yml). -8. Cache and install Playwright (same as live-URL probers). -9. Run: npx playwright test --config=playwright.prober.config.ts docker.spec.ts -10. Upload Playwright HTML report as artifact (if: always()). Set + where the test runs before Caddy is ready to serve requests. This + step runs after Node/Playwright setup so the container doesn't sit + idle during dependency installation. + The Docker spec also includes a guard that checks if localhost:8080 + is reachable before running the prober. If the container is not + running (e.g., when running probers locally without Docker), the + test is skipped with a helpful message instead of failing with a + confusing ECONNREFUSED error. +10. Run: npx playwright test --config=playwright.prober.config.ts docker.spec.ts +11. Upload Playwright HTML report as artifact (if: always()). Set `retention-days: 30` (same as live-URL probers). -11. Stop and remove container (if: always()): docker stop - topola-prober-${{ github.run_id }} 2>/dev/null; docker rm - topola-prober-${{ github.run_id }} 2>/dev/null; true +12. Stop and remove container (if: always()): docker stop + topola-prober-${{ github.run_id }} 2>/dev/null || true; docker rm + topola-prober-${{ github.run_id }} 2>/dev/null || true (The if: always() ensures cleanup runs even on failure. The 2>/dev/null and trailing true prevent errors if the container was never started, e.g., pull failed at step 2.) @@ -700,10 +727,15 @@ Add an entry for this design document to the registry: | `playwright.prober.config.ts` | Create | Separate Playwright config for probers (no local server, live URLs) | | `playwright.config.ts` | Modify | Add `testIgnore` for `probers/**` to e2e project | | `package.json` | Modify | Add `test:probers` script | +| `tests/probers/helpers.ts` | Create | Shared prober flow, diagnostics capture, and selector logic | | `tests/probers/wikitree.spec.ts` | Create | WikiTree direct API smoke test | | `tests/probers/gh-pages-gedcom.spec.ts` | Create | GitHub Pages + CORS proxy smoke test | | `tests/probers/wikitree-cors-gedcom.spec.ts` | Create | WikiTree + CORS proxy smoke test | -| `tests/probers/docker.spec.ts` | Create | Docker container smoke test | +| `tests/probers/docker.spec.ts` | Create | Docker container smoke test (with reachability guard) | +| `src/pages/view_page.tsx` | Modify | Add `data-testid="content"` | +| `src/chart.tsx` | Modify | Add `data-testid="chart"` | +| `src/sidepanel/details/details.tsx` | Modify | Add `data-testid="details"` | +| `src/components/error_display.tsx` | Modify | Add `data-testid` for error message and popup | | `.github/workflows/prober-wikitree.yml` | Create | Reusable workflow: WikiTree prober | | `.github/workflows/prober-gh-pages.yml` | Create | Reusable workflow: GH Pages prober | | `.github/workflows/prober-wikitree-cors.yml` | Create | Reusable workflow: WikiTree CORS prober | diff --git a/package.json b/package.json index 4ded353..7705c5f 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "test:visual": "playwright test --project=visual", "test:visual:update": "playwright test --project=visual --update-snapshots", "test:visual:ui": "playwright test --project=visual --ui", - "test:probers": "playwright test --config=playwright.prober.config.ts gh-pages-gedcom.spec.ts wikitree.spec.ts wikitree-cors-gedcom.spec.ts", + "test:probers": "playwright test --config=playwright.prober.config.ts", "check:all": "prettier --check \"{src,tests}/**/*.{ts,tsx,json}\" && npm run lint && npm run build && npm test && tsc -p tests/tsconfig.json --noEmit && npm run test:e2e && npm run test:visual" }, "homepage": ".", diff --git a/src/chart.tsx b/src/chart.tsx index 74382f1..2338fa9 100644 --- a/src/chart.tsx +++ b/src/chart.tsx @@ -596,7 +596,7 @@ export function Chart(props: ChartProps) { - + ); diff --git a/src/components/error_display.tsx b/src/components/error_display.tsx index 643e933..6b80c5e 100644 --- a/src/components/error_display.tsx +++ b/src/components/error_display.tsx @@ -4,7 +4,7 @@ import {Message, Portal} from 'semantic-ui-react'; /** Shows an error message in the middle of the screen. */ export function ErrorMessage(props: {message?: string}) { return ( - + - + diff --git a/src/pages/view_page.tsx b/src/pages/view_page.tsx index 13d63f6..3af9fac 100644 --- a/src/pages/view_page.tsx +++ b/src/pages/view_page.tsx @@ -199,7 +199,7 @@ export function ViewPage() { } const selection = updatedSelection; return ( -
+
+
{getSectionForEachMatchingEntry( entries, diff --git a/tests/probers/docker.spec.ts b/tests/probers/docker.spec.ts index 45b9133..407d496 100644 --- a/tests/probers/docker.spec.ts +++ b/tests/probers/docker.spec.ts @@ -1,4 +1,6 @@ -import {expect, test} from '@playwright/test'; +import {test} from '@playwright/test'; + +import {runProber} from './helpers'; /** * Prober: Docker container @@ -11,26 +13,36 @@ import {expect, test} from '@playwright/test'; * * The workflow starts the container externally (via `docker run`) before * this test runs, so Playwright connects to localhost:8080 directly. + * + * If the Docker container is not running (e.g., when running probers locally + * without Docker), this test is skipped with a clear message rather than + * failing with a confusing connection error. */ test('Docker container prober', async ({page}) => { - await page.goto('http://localhost:8080/'); + // Guard: verify the Docker container is reachable before running the + // prober. When running locally without Docker, this produces a clear + // skip message instead of a confusing ECONNREFUSED failure. + // page.request.get throws on connection refused (rather than returning a + // non-OK response), so we catch the error and skip. + let containerReachable = false; + try { + const response = await page.request.get('http://localhost:8080/', { + timeout: 5_000, + }); + containerReachable = response.ok(); + } catch { + containerReachable = false; + } + test.skip( + !containerReachable, + 'Docker container is not reachable on localhost:8080 — start it with: ' + + 'docker run -d -p 8080:8080 -e STATIC_URL=test.ged ' + + '-v $(pwd)/src/datasource/testdata/test.ged:/app/public/test.ged ' + + 'ghcr.io/pewu/topola-viewer:latest', + ); - // Wait for the app to reach SHOWING_CHART state. - await expect(page.locator('#content')).toBeVisible(); - - // Assert the expected person's name appears in the chart SVG. - await expect(page.locator('#chart')).toContainText('Bonifacy'); - - // Assert the expected person's name appears in the side panel. - // Scoped to div.details to avoid matching SVG - // elements in the chart that also carry the "details" class. - await expect(page.locator('div.details')).toContainText('Bonifacy'); - - // Assert no fatal error is displayed (replaces chart when state is ERROR). - await expect(page.locator('.ui.error.message')).not.toBeVisible(); - - // Assert no popup error is displayed. - // ErrorPopup uses Semantic UI React's , which renders at - // document.body level, not inside #content. - await expect(page.locator('.ui.errorPopup.message')).not.toBeVisible(); + await runProber(page, { + url: 'http://localhost:8080/', + expectedName: 'Bonifacy', + }); }); diff --git a/tests/probers/gh-pages-gedcom.spec.ts b/tests/probers/gh-pages-gedcom.spec.ts index 9e892e4..95847a7 100644 --- a/tests/probers/gh-pages-gedcom.spec.ts +++ b/tests/probers/gh-pages-gedcom.spec.ts @@ -1,4 +1,6 @@ -import {expect, test} from '@playwright/test'; +import {test} from '@playwright/test'; + +import {runProber} from './helpers'; /** * Prober: GitHub Pages GEDCOM via CORS proxy @@ -10,26 +12,8 @@ import {expect, test} from '@playwright/test'; * deployment, the CORS proxy, and GEDCOM-from-URL loading. */ test('GitHub Pages GEDCOM prober', async ({page}) => { - await page.goto( - 'https://pewu.github.io/topola-viewer/#/view?url=https://raw.githubusercontent.com/PeWu/topola-viewer/master/src/datasource/testdata/test.ged&indi=I1', - ); - - // Wait for the app to reach SHOWING_CHART state. - await expect(page.locator('#content')).toBeVisible(); - - // Assert the expected person's name appears in the chart SVG. - await expect(page.locator('#chart')).toContainText('Bonifacy'); - - // Assert the expected person's name appears in the side panel. - // Scoped to div.details to avoid matching SVG - // elements in the chart that also carry the "details" class. - await expect(page.locator('div.details')).toContainText('Bonifacy'); - - // Assert no fatal error is displayed (replaces chart when state is ERROR). - await expect(page.locator('.ui.error.message')).not.toBeVisible(); - - // Assert no popup error is displayed. - // ErrorPopup uses Semantic UI React's , which renders at - // document.body level, not inside #content. - await expect(page.locator('.ui.errorPopup.message')).not.toBeVisible(); + await runProber(page, { + url: 'https://pewu.github.io/topola-viewer/#/view?url=https://raw.githubusercontent.com/PeWu/topola-viewer/master/src/datasource/testdata/test.ged&indi=I1', + expectedName: 'Bonifacy', + }); }); diff --git a/tests/probers/helpers.ts b/tests/probers/helpers.ts new file mode 100644 index 0000000..011f9bb --- /dev/null +++ b/tests/probers/helpers.ts @@ -0,0 +1,105 @@ +import {expect, type Page} from '@playwright/test'; + +/** + * Registers console, page-error, and network-failure listeners on the given + * page. Playwright captures these in trace files, but also printing them to + * stdout makes them visible in the GitHub Actions log — essential for + * debugging live-URL prober failures where the failure is hard to reproduce. + * + * Listeners are registered once per page and remain active for the test's + * lifetime. + */ +export function captureDiagnostics(page: Page): void { + page.on('console', (msg) => { + const type = msg.type(); + if (type === 'error' || type === 'warning') { + console.log(`[browser ${type}] ${msg.text()}`); + } + }); + page.on('pageerror', (err) => { + console.log(`[page error] ${err.message}`); + }); + page.on('requestfailed', (req) => { + const failure = req.failure(); + console.log( + `[request failed] ${req.url()} — ${failure?.errorText ?? 'unknown'}`, + ); + }); +} + +export interface ProberOptions { + /** Full URL to navigate to. */ + url: string; + /** Expected person name to assert appears in the chart and side panel. */ + expectedName: string; + /** + * Optional navigation timeout in milliseconds. Defaults to 60s — separate + * from the test-level timeout so a hung navigation doesn't eat the entire + * test budget before assertions even begin. + */ + navigationTimeout?: number; +} + +/** + * Runs the standard prober flow against a live URL: + * + * 1. Navigate to the target URL. + * 2. Wait for #content (data-testid="content") to be visible — indicates the + * app reached SHOWING_CHART state. + * 3. Assert the expected person's name appears in the chart SVG. + * 4. Assert the expected person's name appears in the side panel details. + * 5. Assert no fatal error message is displayed. + * 6. Assert no popup error is displayed. + * + * All selectors use `data-testid` attributes for stability — they survive + * CSS class refactors and Semantic UI React internal changes. + */ +export async function runProber( + page: Page, + options: ProberOptions, +): Promise { + const {url, expectedName, navigationTimeout = 60_000} = options; + + captureDiagnostics(page); + + // Use 'domcontentloaded' instead of the default 'load' so we don't wait + // for analytics scripts and third-party resources that are irrelevant to + // the prober's assertions. + await page.goto(url, { + waitUntil: 'domcontentloaded', + timeout: navigationTimeout, + }); + + // Wait for the app to reach SHOWING_CHART state. #content becomes visible + // before the D3 chart SVG is populated — the subsequent #chart text + // assertion relies on Playwright's auto-wait to bridge this gap. + // + // Selectors use a union of data-testid and legacy CSS/ID selectors so the + // prober works against both the currently deployed app (which predates + // data-testid) and future deployments that include the attributes. + const content = page.locator('[data-testid="content"], #content'); + const chart = page.locator('[data-testid="chart"], #chart'); + const details = page.locator('[data-testid="details"], div.details'); + const errorMessage = page.locator( + '[data-testid="error-message"], .ui.error.message', + ); + const errorPopup = page.locator( + '[data-testid="error-popup"], .ui.errorPopup.message', + ); + + await expect(content).toBeVisible(); + + // Assert the expected person's name appears in the chart SVG. + await expect(chart).toContainText(expectedName); + + // Assert the expected person's name appears in the side panel. + await expect(details).toContainText(expectedName); + + // Assert no fatal error is displayed (replaces chart when state is ERROR). + await expect(errorMessage).not.toBeVisible(); + + // Assert no popup error is displayed. ErrorPopup uses Semantic UI React's + // , which renders at document.body level — these locators search + // the entire document, so this works regardless of DOM nesting. + await expect(errorPopup).not.toBeVisible(); +} diff --git a/tests/probers/wikitree-cors-gedcom.spec.ts b/tests/probers/wikitree-cors-gedcom.spec.ts index e9cec75..6d6541c 100644 --- a/tests/probers/wikitree-cors-gedcom.spec.ts +++ b/tests/probers/wikitree-cors-gedcom.spec.ts @@ -1,4 +1,6 @@ -import {expect, test} from '@playwright/test'; +import {test} from '@playwright/test'; + +import {runProber} from './helpers'; /** * Prober: WikiTree GEDCOM + CORS proxy @@ -9,26 +11,8 @@ import {expect, test} from '@playwright/test'; * the CORS proxy is reachable from the WikiTree deployment. */ test('WikiTree CORS proxy prober', async ({page}) => { - await page.goto( - 'https://apps.wikitree.com/apps/wiech13/topola-viewer/#/view?url=https://raw.githubusercontent.com/PeWu/topola-viewer/master/src/datasource/testdata/test.ged&indi=I1', - ); - - // Wait for the app to reach SHOWING_CHART state. - await expect(page.locator('#content')).toBeVisible(); - - // Assert the expected person's name appears in the chart SVG. - await expect(page.locator('#chart')).toContainText('Bonifacy'); - - // Assert the expected person's name appears in the side panel. - // Scoped to div.details to avoid matching SVG - // elements in the chart that also carry the "details" class. - await expect(page.locator('div.details')).toContainText('Bonifacy'); - - // Assert no fatal error is displayed (replaces chart when state is ERROR). - await expect(page.locator('.ui.error.message')).not.toBeVisible(); - - // Assert no popup error is displayed. - // ErrorPopup uses Semantic UI React's , which renders at - // document.body level, not inside #content. - await expect(page.locator('.ui.errorPopup.message')).not.toBeVisible(); + await runProber(page, { + url: 'https://apps.wikitree.com/apps/wiech13/topola-viewer/#/view?url=https://raw.githubusercontent.com/PeWu/topola-viewer/master/src/datasource/testdata/test.ged&indi=I1', + expectedName: 'Bonifacy', + }); }); diff --git a/tests/probers/wikitree.spec.ts b/tests/probers/wikitree.spec.ts index 8edb429..ef5da8a 100644 --- a/tests/probers/wikitree.spec.ts +++ b/tests/probers/wikitree.spec.ts @@ -1,4 +1,6 @@ -import {expect, test} from '@playwright/test'; +import {test} from '@playwright/test'; + +import {runProber} from './helpers'; /** * Prober: WikiTree direct API @@ -9,26 +11,8 @@ import {expect, test} from '@playwright/test'; * direct API path and confirms the WikiTree deployment is healthy. */ test('WikiTree direct API prober', async ({page}) => { - await page.goto( - 'https://apps.wikitree.com/apps/wiech13/topola-viewer/#/view?source=wikitree&indi=Sk%C5%82odowska-2', - ); - - // Wait for the app to reach SHOWING_CHART state. - await expect(page.locator('#content')).toBeVisible(); - - // Assert the expected person's name appears in the chart SVG. - await expect(page.locator('#chart')).toContainText('Skłodowska'); - - // Assert the expected person's name appears in the side panel. - // Scoped to div.details to avoid matching SVG - // elements in the chart that also carry the "details" class. - await expect(page.locator('div.details')).toContainText('Skłodowska'); - - // Assert no fatal error is displayed (replaces chart when state is ERROR). - await expect(page.locator('.ui.error.message')).not.toBeVisible(); - - // Assert no popup error is displayed. - // ErrorPopup uses Semantic UI React's , which renders at - // document.body level, not inside #content. - await expect(page.locator('.ui.errorPopup.message')).not.toBeVisible(); + await runProber(page, { + url: 'https://apps.wikitree.com/apps/wiech13/topola-viewer/#/view?source=wikitree&indi=Sk%C5%82odowska-2', + expectedName: 'Skłodowska', + }); });