mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-07-17 17:21:48 +00:00
refactor: consolidate prober logic into shared helper and add test-testid attributes to core UI elements
This commit is contained in:
22
.github/workflows/prober-docker.yml
vendored
22
.github/workflows/prober-docker.yml
vendored
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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": ".",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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/');
|
||||
|
||||
// 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();
|
||||
// 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',
|
||||
);
|
||||
|
||||
await runProber(page, {
|
||||
url: 'http://localhost:8080/',
|
||||
expectedName: 'Bonifacy',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
105
tests/probers/helpers.ts
Normal 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();
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user