Add screenshot tests
@@ -4,7 +4,7 @@ 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');
|
||||
await page.goto('/#/view?url=https://example.org/family.ged');
|
||||
});
|
||||
|
||||
test('loads data from URL', async ({page}) => {
|
||||
|
||||
27
tests/charts_visual.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {setupGedcomRoute} from './helpers';
|
||||
|
||||
test.describe('Core SVG Canvas Layouts @visual', () => {
|
||||
test.beforeEach(async ({page, context}) => {
|
||||
await setupGedcomRoute(context);
|
||||
});
|
||||
|
||||
const layouts = [
|
||||
{view: 'hourglass', selector: '#svgContainer', waitTime: 500},
|
||||
{view: 'relatives', selector: '#svgContainer', waitTime: 500},
|
||||
{view: 'donatso', selector: '#dotatsoSvgContainer', waitTime: 1500},
|
||||
];
|
||||
|
||||
for (const layout of layouts) {
|
||||
test(`chart-${layout.view}`, async ({page}) => {
|
||||
await page.goto(
|
||||
`/#/view?url=https://example.org/family.ged&view=${layout.view}`,
|
||||
);
|
||||
const container = page.locator(layout.selector);
|
||||
await container.waitFor({state: 'visible'});
|
||||
// Wait for D3 rendering and layout stabilization.
|
||||
await page.waitForTimeout(layout.waitTime);
|
||||
await expect(container).toHaveScreenshot(`chart-${layout.view}.png`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 46 KiB |
59
tests/config_visual.spec.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {setupGedcomRoute} from './helpers';
|
||||
|
||||
test.describe('Configurations Integration @visual', () => {
|
||||
test.beforeEach(async ({page, context}) => {
|
||||
await setupGedcomRoute(context);
|
||||
await page.goto('/#/view?url=https://example.org/family.ged');
|
||||
|
||||
// Wait for the sidebar and the main content container to be visible.
|
||||
const sidebar = page.locator('#sidebar');
|
||||
const mainContent = page.locator('#content');
|
||||
await sidebar.waitFor();
|
||||
await mainContent.waitFor();
|
||||
|
||||
// Switch to the Settings/Config tab in the side panel
|
||||
await page.getByText('Settings', {exact: true}).click();
|
||||
});
|
||||
|
||||
test('Default Configuration (State 1)', async ({page}) => {
|
||||
// Assert the Default Configuration (State 1).
|
||||
await expect(page).toHaveScreenshot('config-state-default.png');
|
||||
});
|
||||
|
||||
test('Sex Colors & No IDs Configuration (State 2)', async ({page}) => {
|
||||
// Locate the section containers inside form.details specifically to avoid Info tab ambiguity.
|
||||
const colorsSection = page
|
||||
.locator('form.details .item')
|
||||
.filter({hasText: 'Colors'});
|
||||
const idsSection = page
|
||||
.locator('form.details .item')
|
||||
.filter({hasText: 'IDs'});
|
||||
|
||||
// Toggle "by sex" colors and "hide" IDs.
|
||||
await colorsSection.getByText('by sex').click();
|
||||
await idsSection.getByText('hide').click();
|
||||
|
||||
// Wait a brief moment for SVG rendering to update.
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page).toHaveScreenshot('config-state-gender-no-ids.png');
|
||||
});
|
||||
|
||||
test('Minimalist Configuration (State 3)', async ({page}) => {
|
||||
// Locate the section containers inside form.details specifically to avoid Info tab ambiguity.
|
||||
const colorsSection = page
|
||||
.locator('form.details .item')
|
||||
.filter({hasText: 'Colors'});
|
||||
const sexSection = page
|
||||
.locator('form.details .item')
|
||||
.filter({hasText: 'Sex'});
|
||||
|
||||
// Toggle "none" colors and "hide" sex labels.
|
||||
await colorsSection.getByText('none').click();
|
||||
await sexSection.getByText('hide').click();
|
||||
|
||||
// Wait a brief moment for SVG rendering to update.
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page).toHaveScreenshot('config-state-minimalist.png');
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 54 KiB |
138
tests/details_visual.spec.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import {expect, test} from '@playwright/test';
|
||||
import dedent from 'dedent';
|
||||
import * as fs from 'fs';
|
||||
import {blockTracking, mockGedcomResponse} from './helpers';
|
||||
|
||||
test.describe('Details panel visual validation @visual', () => {
|
||||
test.beforeEach(async ({context}) => {
|
||||
await blockTracking(context);
|
||||
});
|
||||
|
||||
test('Complex Names Test', async ({page, context}) => {
|
||||
const complexNameGedcom = dedent`
|
||||
0 HEAD
|
||||
1 GEDC
|
||||
2 VERS 5.5.1
|
||||
2 FORM Lineage-Linked
|
||||
1 CHAR UTF-8
|
||||
0 @I1@ INDI
|
||||
1 NAME Dr. Bonifacy "Boni" /Gibbs/ III
|
||||
2 NPFX Dr.
|
||||
2 GIVN Bonifacy
|
||||
2 NICK Boni
|
||||
2 SURN Gibbs
|
||||
2 NSFX III
|
||||
2 _RUFNAME Bonifacy
|
||||
1 SEX M
|
||||
1 FAMS @F1@
|
||||
0 @F1@ FAM
|
||||
1 HUSB @I1@
|
||||
0 TRLR
|
||||
`;
|
||||
|
||||
await mockGedcomResponse(context, complexNameGedcom);
|
||||
|
||||
await page.goto('/#/view?url=https://example.org/family.ged');
|
||||
const sidebar = page.locator('#sidebar');
|
||||
await sidebar.waitFor();
|
||||
await expect(sidebar).toHaveScreenshot('details-complex-name.png');
|
||||
});
|
||||
|
||||
test('Image / Photo Rendering Test', async ({page, context}) => {
|
||||
const photoGedcom = dedent`
|
||||
0 HEAD
|
||||
1 GEDC
|
||||
2 VERS 5.5.1
|
||||
2 FORM Lineage-Linked
|
||||
1 CHAR UTF-8
|
||||
0 @I1@ INDI
|
||||
1 NAME Bonifacy /Gibbs/
|
||||
1 SEX M
|
||||
1 FAMS @F1@
|
||||
1 OBJE @O1@
|
||||
0 @O1@ OBJE
|
||||
1 FILE http://example.org/photos/I1.jpg
|
||||
2 FORM jpeg
|
||||
0 @F1@ FAM
|
||||
1 HUSB @I1@
|
||||
0 TRLR
|
||||
`;
|
||||
|
||||
await mockGedcomResponse(context, photoGedcom);
|
||||
|
||||
await context.route('**/photos/I1.jpg', async (route) => {
|
||||
const imageBuffer = fs.readFileSync(
|
||||
'docker/examples/photos/photos/I1.jpg',
|
||||
);
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'image/jpeg',
|
||||
body: imageBuffer,
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/#/view?url=https://example.org/family.ged');
|
||||
const sidebar = page.locator('#sidebar');
|
||||
await sidebar.waitFor();
|
||||
|
||||
// Wait for image loading to complete
|
||||
const img = sidebar.locator('img').first();
|
||||
await img.waitFor({state: 'visible'});
|
||||
await img.evaluate((image) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if ((image as HTMLImageElement).complete) resolve(true);
|
||||
image.addEventListener('load', () => resolve(true));
|
||||
image.addEventListener('error', () =>
|
||||
reject(new Error('Image failed to load')),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await expect(sidebar).toHaveScreenshot('details-photo-render.png');
|
||||
});
|
||||
|
||||
test('Custom Facts & Citations Test', async ({page, context}) => {
|
||||
const customFactsGedcom = dedent`
|
||||
0 HEAD
|
||||
1 GEDC
|
||||
2 VERS 5.5.1
|
||||
2 FORM Lineage-Linked
|
||||
1 CHAR UTF-8
|
||||
0 @I1@ INDI
|
||||
1 NAME Bonifacy /Gibbs/
|
||||
1 SEX M
|
||||
1 FAMS @F1@
|
||||
1 FACT Custom fact data
|
||||
2 TYPE Custom Fact Type
|
||||
2 SOUR @S1@
|
||||
3 PAGE 42
|
||||
3 DATA
|
||||
4 DATE 12 JAN 1850
|
||||
2 NOTE This is a note nested under a custom fact.
|
||||
1 BIRT
|
||||
2 DATE 1 JAN 1800
|
||||
2 PLAC Paris, France
|
||||
2 SOUR @S1@
|
||||
3 PAGE 10
|
||||
2 NOTE Birth event note.
|
||||
1 DEAT
|
||||
2 DATE 31 DEC 1880
|
||||
2 PLAC London, UK
|
||||
1 NOTE This is a top-level note for the individual.
|
||||
0 @S1@ SOUR
|
||||
1 TITL Great Genealogy Book
|
||||
1 AUTH John Doe
|
||||
1 PUBL London Publishing, 1890
|
||||
0 @F1@ FAM
|
||||
1 HUSB @I1@
|
||||
0 TRLR
|
||||
`;
|
||||
|
||||
await mockGedcomResponse(context, customFactsGedcom);
|
||||
|
||||
await page.goto('/#/view?url=https://example.org/family.ged');
|
||||
const sidebar = page.locator('#sidebar');
|
||||
await sidebar.waitFor();
|
||||
await expect(sidebar).toHaveScreenshot('details-events-sources.png');
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 104 KiB |
@@ -10,6 +10,23 @@ export async function blockTracking(context: BrowserContext): Promise<void> {
|
||||
await context.route('**/*googletagmanager.com/**', (route) => route.abort());
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks the endpoint for GEDCOM file requests using the provided GEDCOM content string.
|
||||
*/
|
||||
export async function mockGedcomResponse(
|
||||
context: BrowserContext,
|
||||
gedcomContent: string,
|
||||
): Promise<void> {
|
||||
await context.route('**/family.ged', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/plain',
|
||||
headers: {'Access-Control-Allow-Origin': '*'},
|
||||
body: gedcomContent,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up interception for raw GEDCOM requests, fulfills them with cached test data
|
||||
* and sets up CORS proxy checks and analytics blocking.
|
||||
@@ -20,14 +37,6 @@ export async function setupGedcomRoute(context: BrowserContext): Promise<void> {
|
||||
'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 mockGedcomResponse(context, gedcomContent);
|
||||
await blockTracking(context);
|
||||
}
|
||||
|
||||
37
tests/intro_visual.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {blockTracking} from './helpers';
|
||||
|
||||
test.describe('Intro page visual validation @visual', () => {
|
||||
test.beforeEach(async ({page, context}) => {
|
||||
await blockTracking(context);
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('intro-page', async ({page}) => {
|
||||
// Clean dynamic elements right before snapping the screenshot.
|
||||
await page.evaluate(() => {
|
||||
// 1. Overwrite dynamic footer versioning.
|
||||
const versionEl = document.querySelector('.version');
|
||||
if (versionEl) {
|
||||
(versionEl as HTMLElement).innerText =
|
||||
'version: 2026-01-01 00:00 (testcommit)';
|
||||
}
|
||||
|
||||
// 2. Replace dynamic changelog block with static placeholder.
|
||||
const headers = Array.from(document.querySelectorAll('h3'));
|
||||
const whatsNewHeader = headers.find((h) =>
|
||||
h.textContent?.includes("What's new"),
|
||||
);
|
||||
if (whatsNewHeader) {
|
||||
const changelogSpan = whatsNewHeader.nextElementSibling;
|
||||
if (changelogSpan) {
|
||||
changelogSpan.innerHTML =
|
||||
'<h4>2026-01-01</h4><ul><li>Placeholder change entry</li></ul>';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Snap the screenshot.
|
||||
await expect(page).toHaveScreenshot('intro-page.png');
|
||||
});
|
||||
});
|
||||
BIN
tests/intro_visual.spec.ts-snapshots/intro-page-visual-linux.png
Normal file
|
After Width: | Height: | Size: 233 KiB |
@@ -7,7 +7,7 @@ test.describe('Search functionality', () => {
|
||||
});
|
||||
|
||||
test('Search works', async ({page}) => {
|
||||
await page.goto('/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged');
|
||||
await page.goto('/#/view?url=https://example.org/family.ged');
|
||||
await expect(page.locator('#content')).not.toContainText('Chike');
|
||||
|
||||
const searchInput = page.getByPlaceholder('Search for people');
|
||||
|
||||
@@ -32,7 +32,7 @@ test.describe('WebMCP Integration', () => {
|
||||
});
|
||||
|
||||
test('registers tools to standard modelContext', async ({page}) => {
|
||||
await page.goto('/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged');
|
||||
await page.goto('/#/view?url=https://example.org/family.ged');
|
||||
|
||||
// Polling assertion to avoid React useEffect registration race condition.
|
||||
await page.waitForFunction(
|
||||
@@ -49,7 +49,7 @@ test.describe('WebMCP Integration', () => {
|
||||
});
|
||||
|
||||
test('allows running focus_indi tool', async ({page}) => {
|
||||
await page.goto('/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged');
|
||||
await page.goto('/#/view?url=https://example.org/family.ged');
|
||||
await page.waitForFunction(
|
||||
(expectedCount) => window.__registeredTools?.length === expectedCount,
|
||||
EXPECTED_TOOL_NAMES.length,
|
||||
|
||||