refactor: consolidate prober logic into shared helper and add test-testid attributes to core UI elements

This commit is contained in:
Przemek Więch
2026-07-04 21:42:21 +02:00
parent fa2636e8b1
commit 980df35198
12 changed files with 245 additions and 138 deletions

View File

@@ -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

View File

@@ -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 `<Portal>` 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 `<Portal>` 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 `<Message>`
@@ -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 |

View File

@@ -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": ".",

View File

@@ -596,7 +596,7 @@ export function Chart(props: ChartProps) {
</button>
</Media>
<svg id="chartSvg">
<g id="chart" />
<g id="chart" data-testid="chart" />
</svg>
</div>
);

View File

@@ -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 (
<Message negative className="error">
<Message negative className="error" data-testid="error-message">
<Message.Header>
<FormattedMessage
id="error.failed_to_load_file"
@@ -28,7 +28,7 @@ export interface ErrorPopupProps {
export function ErrorPopup(props: ErrorPopupProps) {
return (
<Portal open={props.open} onClose={props.onDismiss}>
<Message negative className="errorPopup" onDismiss={props.onDismiss}>
<Message negative className="errorPopup" onDismiss={props.onDismiss} data-testid="error-popup">
<Message.Header>
<FormattedMessage id="error.error" defaultMessage={'Error'} />
</Message.Header>

View File

@@ -199,7 +199,7 @@ export function ViewPage() {
}
const selection = updatedSelection;
return (
<div id="content">
<div id="content" data-testid="content">
<ErrorPopup
open={showErrorPopup}
message={error}

View File

@@ -354,7 +354,7 @@ export function Details(props: Props) {
const entries = props.gedcom.indis[props.indi].tree;
return (
<div className="details">
<div className="details" data-testid="details">
<Item.Group divided>
{getSectionForEachMatchingEntry(
entries,

View File

@@ -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 <text class="details sex"> 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 <Portal>, 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',
});
});

View File

@@ -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 <text class="details sex"> 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 <Portal>, 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',
});
});

105
tests/probers/helpers.ts Normal file
View File

@@ -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<void> {
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
// <Portal>, which renders at document.body level — these locators search
// the entire document, so this works regardless of DOM nesting.
await expect(errorPopup).not.toBeVisible();
}

View File

@@ -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 <text class="details sex"> 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 <Portal>, 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',
});
});

View File

@@ -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 <text class="details sex"> 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 <Portal>, 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',
});
});