Migrate from Cypress to Playwright

This commit is contained in:
Przemek Więch
2026-05-06 11:51:30 +02:00
parent 664ca69fb3
commit d427300ddf
28 changed files with 740 additions and 1403 deletions

26
tests/chart_view.spec.ts Normal file
View File

@@ -0,0 +1,26 @@
import {expect, test} from '@playwright/test';
import {setupGedcomRoute} from './helpers';
test.describe('Chart view', () => {
test.beforeEach(async ({page, context}) => {
await setupGedcomRoute(context);
await page.goto('/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged');
});
test('loads data from URL', async ({page}) => {
await expect(page.locator('#content')).toContainText('Bonifacy');
});
test('Animates chart', async ({page}) => {
await expect(page.locator('#content')).not.toContainText('Chike');
// Click Radobod's node. force: true is required because D3 wraps the text in a border <rect>
// that intercepts pointer events, which is expected SVG chart layout behavior.
await page.getByText('Radobod').click({force: true});
await expect(page.locator('#content')).toContainText('Chike');
});
test('shows the right panel', async ({page}) => {
await expect(page.locator('#content')).toContainText('a random note');
});
});

33
tests/embedded.spec.ts Normal file
View File

@@ -0,0 +1,33 @@
import {expect, test} from '@playwright/test';
import * as fs from 'fs';
import {setupGedcomRoute} from './helpers';
test.describe('Embedded mode', () => {
test('shows data', async ({page, context}) => {
// Intercept family.ged requests coming from parent frame.
await setupGedcomRoute(context);
// Read the physical HTML wrapper template file.
const wrapperHtml = fs.readFileSync(
'tests/fixtures/embedded_frame.html',
'utf-8',
);
// Route parent page wrapper virtually on the same origin/port dynamically.
await context.route(`**/test-embedded-frame.html`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'text/html',
body: wrapperHtml,
});
});
// Load the virtual wrapper page.
await page.goto(`/test-embedded-frame.html`);
// Assert child iframe successfully loaded Bonifacy Gibbs.
const iframe = page.frameLocator('#topolaFrame');
await expect(iframe.locator('#root')).toContainText('Bonifacy');
});
});

44
tests/fixtures/embedded_frame.html vendored Normal file
View File

@@ -0,0 +1,44 @@
<!doctype html>
<html>
<head>
<title>Embedded Frame Test Wrapper</title>
<script>
var gedcom = null;
var ready = false;
var topolaFrameId = 'topolaFrame';
var gedcomUrl =
'https://raw.githubusercontent.com/PeWu/topola/master/demo/data/family.ged';
function maybeSendData() {
if (!ready || !gedcom) return;
var frame = document.getElementById(topolaFrameId);
frame.contentWindow.postMessage({message: 'gedcom', gedcom});
}
window.addEventListener('message', function (message) {
if (
message.data &&
message.data.message === 'ready'
) {
ready = true;
maybeSendData();
}
});
window
.fetch(gedcomUrl)
.then((response) => response.text())
.then((data) => {
gedcom = data;
maybeSendData();
});
</script>
</head>
<body>
<iframe
id="topolaFrame"
src="/#/view?embedded=true&handleCors=false"
style="width: 100%; height: 800px; border: none"
></iframe>
</body>
</html>

3
tests/global.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
interface Window {
__registeredTools?: any[];
}

33
tests/helpers.ts Normal file
View File

@@ -0,0 +1,33 @@
import {BrowserContext} from '@playwright/test';
import * as fs from 'fs';
/**
* Blocks external tracking services (like Google Analytics and Google Tag Manager)
* to guarantee a hermetic and fast test environment.
*/
export async function blockTracking(context: BrowserContext): Promise<void> {
await context.route('**/*google-analytics.com/**', (route) => route.abort());
await context.route('**/*googletagmanager.com/**', (route) => route.abort());
}
/**
* Sets up interception for raw GEDCOM requests, fulfills them with cached test data
* and sets up CORS proxy checks and analytics blocking.
*/
export async function setupGedcomRoute(context: BrowserContext): Promise<void> {
const gedcomContent = fs.readFileSync(
'src/datasource/testdata/test.ged',
'utf-8',
);
await context.route('**/family.ged', async (route) => {
await route.fulfill({
status: 200,
contentType: 'text/plain',
headers: {'Access-Control-Allow-Origin': '*'},
body: gedcomContent,
});
});
await blockTracking(context);
}

18
tests/intro.spec.ts Normal file
View File

@@ -0,0 +1,18 @@
import {expect, test} from '@playwright/test';
import {blockTracking} from './helpers';
test.describe('Intro page', () => {
test.beforeEach(async ({page, context}) => {
await blockTracking(context);
await page.goto('/');
});
test('displays intro text', async ({page}) => {
await expect(page.getByText('Examples')).toBeVisible();
});
test('displays menu', async ({page}) => {
await expect(page.getByText('Open file', {exact: true})).toBeVisible();
await expect(page.getByText('Load from URL', {exact: true})).toBeVisible();
});
});

22
tests/search.spec.ts Normal file
View File

@@ -0,0 +1,22 @@
import {expect, test} from '@playwright/test';
import {setupGedcomRoute} from './helpers';
test.describe('Search functionality', () => {
test.beforeEach(async ({context}) => {
await setupGedcomRoute(context);
});
test('Search works', async ({page}) => {
await page.goto('/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged');
await expect(page.locator('#content')).not.toContainText('Chike');
const searchInput = page.getByPlaceholder('Search for people');
await searchInput.fill('chik');
// Wait for the debounced suggestion panel to render.
await expect(page.locator('.results')).toContainText('Chike');
await searchInput.press('Enter');
await expect(page.locator('#content')).toContainText('Chike');
});
});

11
tests/tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "NodeNext",
"target": "ES2022",
"moduleResolution": "NodeNext",
"types": ["node"],
"noEmit": true
},
"include": ["./**/*.ts", "./**/*.d.ts"]
}

73
tests/webmcp.spec.ts Normal file
View File

@@ -0,0 +1,73 @@
import {expect, test} from '@playwright/test';
import {setupGedcomRoute} from './helpers';
const EXPECTED_TOOL_NAMES = [
'get_selected_person',
'search_indi',
'inspect_indi',
'focus_indi',
'find_relationship_path',
'get_ancestors',
'get_descendants',
];
test.describe('WebMCP Integration', () => {
test.beforeEach(async ({page, context}) => {
await setupGedcomRoute(context);
// Add init script to expose modelContext mock BEFORE application boots.
await page.addInitScript(() => {
const registeredTools: any[] = [];
window.__registeredTools = registeredTools;
window.navigator.modelContext = {
registerTool: (tool: any) => {
registeredTools.push(tool);
},
unregisterTool: (name: string) => {
const idx = registeredTools.findIndex((t) => t.name === name);
if (idx !== -1) registeredTools.splice(idx, 1);
},
};
});
});
test('registers tools to standard modelContext', async ({page}) => {
await page.goto('/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged');
// Polling assertion to avoid React useEffect registration race condition.
await page.waitForFunction(
(expectedCount) => window.__registeredTools?.length === expectedCount,
EXPECTED_TOOL_NAMES.length,
);
const toolNames = await page.evaluate(() =>
window.__registeredTools
? window.__registeredTools.map((t: any) => t.name)
: [],
);
expect(toolNames.sort()).toEqual([...EXPECTED_TOOL_NAMES].sort());
});
test('allows running focus_indi tool', async ({page}) => {
await page.goto('/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged');
await page.waitForFunction(
(expectedCount) => window.__registeredTools?.length === expectedCount,
EXPECTED_TOOL_NAMES.length,
);
await expect(page.locator('#content')).toContainText('Radobod');
await expect(page.locator('#content')).not.toContainText('Chike');
// Execute the non-serializable callback inside the browser environment.
await page.evaluate(async () => {
const focusTool = window.__registeredTools
? window.__registeredTools.find((t: any) => t.name === 'focus_indi')
: null;
if (!focusTool) throw new Error('focus_indi tool not found');
await focusTool.execute({id: 'I21'}); // Shifts view focus to Chike.
});
// Verify that the UI updated automatically in response to the tool action.
await expect(page.locator('#content')).toContainText('Chike');
});
});