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:
@@ -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',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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