mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-10-22 13:44:06 +00:00
Migrate from Cypress to Playwright
This commit is contained in:
26
tests/chart_view.spec.ts
Normal file
26
tests/chart_view.spec.ts
Normal 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
33
tests/embedded.spec.ts
Normal 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
44
tests/fixtures/embedded_frame.html
vendored
Normal 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
3
tests/global.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
interface Window {
|
||||
__registeredTools?: any[];
|
||||
}
|
||||
33
tests/helpers.ts
Normal file
33
tests/helpers.ts
Normal 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
18
tests/intro.spec.ts
Normal 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
22
tests/search.spec.ts
Normal 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
11
tests/tsconfig.json
Normal 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
73
tests/webmcp.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user