mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-07-17 17:21:48 +00:00
Format markdown files
This commit is contained in:
12
CHANGELOG.md
12
CHANGELOG.md
@@ -10,15 +10,18 @@
|
||||
|
||||
## 2026-04-20
|
||||
|
||||
- Shift-click to show a person's details without rearranging the tree (by FrankBuchholz)
|
||||
- Shift-click to show a person's details without rearranging the tree (by
|
||||
FrankBuchholz)
|
||||
|
||||
## 2026-02-21
|
||||
|
||||
- Improved saving PDF files. Decreased file size and increased chart size that can be saved as PDF.
|
||||
- Improved saving PDF files. Decreased file size and increased chart size that
|
||||
can be saved as PDF.
|
||||
|
||||
## 2026-02-13
|
||||
|
||||
- Show header information of the gedcom file on the side panel (by FrankBuchholz)
|
||||
- Show header information of the gedcom file on the side panel (by
|
||||
FrankBuchholz)
|
||||
|
||||
## 2026-01-19
|
||||
|
||||
@@ -30,7 +33,8 @@
|
||||
|
||||
## 2025-01-21
|
||||
|
||||
- Added new chart type based on [github.com/donatso/family-chart](https://github.com/donatso/family-chart)
|
||||
- Added new chart type based on
|
||||
[github.com/donatso/family-chart](https://github.com/donatso/family-chart)
|
||||
|
||||
## 2023-08-25
|
||||
|
||||
|
||||
@@ -1,56 +1,93 @@
|
||||
# Project Structure
|
||||
|
||||
This directory is the root directory of the **Topola Genealogy Viewer** project. It serves as the central hub containing project configuration, build setup, dependency management, and the main entry points for development, testing, and deployment. The actual application source code, assets, and tests are organized into subdirectories.
|
||||
This directory is the root directory of the **Topola Genealogy Viewer** project.
|
||||
It serves as the central hub containing project configuration, build setup,
|
||||
dependency management, and the main entry points for development, testing, and
|
||||
deployment. The actual application source code, assets, and tests are organized
|
||||
into subdirectories.
|
||||
|
||||
## Purpose of this Directory
|
||||
|
||||
The root directory orchestrates the project. It contains configuration files for various tools (Vite, ESLint, Jest, Playwright, TypeScript), metadata about dependencies (`package.json`), and scripts for common tasks. It glues together the source code in `src`, the static assets in `public`, and the tests in `tests`.
|
||||
The root directory orchestrates the project. It contains configuration files for
|
||||
various tools (Vite, ESLint, Jest, Playwright, TypeScript), metadata about
|
||||
dependencies (`package.json`), and scripts for common tasks. It glues together
|
||||
the source code in `src`, the static assets in `public`, and the tests in
|
||||
`tests`.
|
||||
|
||||
## Files in this Directory (Categorized by Role)
|
||||
|
||||
Here is an enumeration of the files in this directory, categorized by their role in the project:
|
||||
Here is an enumeration of the files in this directory, categorized by their role
|
||||
in the project:
|
||||
|
||||
### Application Entry Point
|
||||
* **[index.html](index.html)**: The main HTML entry point for the application.
|
||||
|
||||
- **[index.html](index.html)**: The main HTML entry point for the application.
|
||||
|
||||
### Build & Environment
|
||||
* **[.env](.env)**: Contains environment variables for the project.
|
||||
* **[vite.config.mts](vite.config.mts)**: Configuration file for the Vite build tool.
|
||||
|
||||
- **[.env](.env)**: Contains environment variables for the project.
|
||||
- **[vite.config.mts](vite.config.mts)**: Configuration file for the Vite build
|
||||
tool.
|
||||
|
||||
### Dependencies & Scripts
|
||||
* **[package.json](package.json)**: Defines the project's dependencies, scripts, version, and other metadata.
|
||||
* **[package-lock.json](package-lock.json)**: Automatically generated file that describes the exact dependency tree that was generated by npm.
|
||||
* **[deploy-wikitree.sh](deploy-wikitree.sh)**: A shell script used to deploy the application files to WikiTree servers using `lftp`.
|
||||
|
||||
- **[package.json](package.json)**: Defines the project's dependencies, scripts,
|
||||
version, and other metadata.
|
||||
- **[package-lock.json](package-lock.json)**: Automatically generated file that
|
||||
describes the exact dependency tree that was generated by npm.
|
||||
- **[deploy-wikitree.sh](deploy-wikitree.sh)**: A shell script used to deploy
|
||||
the application files to WikiTree servers using `lftp`.
|
||||
|
||||
### Tool Configuration
|
||||
* **[.eslintrc.js](.eslintrc.js)**: Configuration file for ESLint, used for identifying and reporting on patterns in JavaScript/TypeScript code.
|
||||
* **[.gitignore](.gitignore)**: Specifies files and directories that should be ignored by Git.
|
||||
* **[prettier.config.mjs](prettier.config.mjs)**: Configuration for Prettier code formatter.
|
||||
* **[tsconfig.json](tsconfig.json)**: Configuration file for the TypeScript compiler.
|
||||
|
||||
- **[.eslintrc.js](.eslintrc.js)**: Configuration file for ESLint, used for
|
||||
identifying and reporting on patterns in JavaScript/TypeScript code.
|
||||
- **[.gitignore](.gitignore)**: Specifies files and directories that should be
|
||||
ignored by Git.
|
||||
- **[prettier.config.mjs](prettier.config.mjs)**: Configuration for Prettier
|
||||
code formatter.
|
||||
- **[tsconfig.json](tsconfig.json)**: Configuration file for the TypeScript
|
||||
compiler.
|
||||
|
||||
### Testing
|
||||
* **[playwright.config.ts](playwright.config.ts)**: Configuration file for Playwright end-to-end testing framework.
|
||||
* **[playwright.prober.config.ts](playwright.prober.config.ts)**: Separate Playwright config for prober (live smoke) tests against deployed URLs.
|
||||
* **[jest.config.ts](jest.config.ts)**: Configuration file for the Jest testing framework used for unit tests.
|
||||
|
||||
- **[playwright.config.ts](playwright.config.ts)**: Configuration file for
|
||||
Playwright end-to-end testing framework.
|
||||
- **[playwright.prober.config.ts](playwright.prober.config.ts)**: Separate
|
||||
Playwright config for prober (live smoke) tests against deployed URLs.
|
||||
- **[jest.config.ts](jest.config.ts)**: Configuration file for the Jest testing
|
||||
framework used for unit tests.
|
||||
|
||||
### Documentation & Assets
|
||||
* **[README.md](README.md)**: The main project documentation, covering features, examples, and usage instructions.
|
||||
* **[CHANGELOG.md](CHANGELOG.md)**: Records a log of notable changes made to the project over time.
|
||||
* **[docs/README.md](docs/README.md)**: Registry for system design files describing containerization, AI protocol interfaces, Playwright integration, and screenshot testing.
|
||||
* **[LICENSE](LICENSE)**: The license file for the project (Apache License 2.0).
|
||||
* **[screenshot.png](screenshot.png)**: An image showing a screenshot of the application, used in the README.md.
|
||||
|
||||
- **[README.md](README.md)**: The main project documentation, covering features,
|
||||
examples, and usage instructions.
|
||||
- **[CHANGELOG.md](CHANGELOG.md)**: Records a log of notable changes made to the
|
||||
project over time.
|
||||
- **[docs/README.md](docs/README.md)**: Registry for system design files
|
||||
describing containerization, AI protocol interfaces, Playwright integration,
|
||||
and screenshot testing.
|
||||
- **[LICENSE](LICENSE)**: The license file for the project (Apache License 2.0).
|
||||
- **[screenshot.png](screenshot.png)**: An image showing a screenshot of the
|
||||
application, used in the README.md.
|
||||
|
||||
## Subdirectories Overview
|
||||
|
||||
Based on the documentation in the subdirectories, here is a high-level overview of the project's structure:
|
||||
Based on the documentation in the subdirectories, here is a high-level overview
|
||||
of the project's structure:
|
||||
|
||||
* **[docs](docs)**: High-level technical and architectural design documents.
|
||||
* **[src](src)**: The core of the application.
|
||||
* **[datasource](src/datasource)**: Handles loading and transforming genealogical data from various sources (files, URLs, WikiTree).
|
||||
* **[menu](src/menu)**: Contains navigation and menu components.
|
||||
* **[sidepanel](src/sidepanel)**: Implements the side panel for details and configuration.
|
||||
* **[translations](src/translations)**: Holds localization JSON files.
|
||||
* **[util](src/util)**: Common utilities for dates, analytics, and data processing.
|
||||
* **[public](public)**: Contains static assets (like the favicon) served directly at the root path.
|
||||
* **[tests](tests)**: Contains end-to-end tests.
|
||||
* **[tests/probers](tests/probers)**: Live smoke tests (probers) that run against deployed URLs and external services.
|
||||
- **[docs](docs)**: High-level technical and architectural design documents.
|
||||
- **[src](src)**: The core of the application.
|
||||
- **[datasource](src/datasource)**: Handles loading and transforming
|
||||
genealogical data from various sources (files, URLs, WikiTree).
|
||||
- **[menu](src/menu)**: Contains navigation and menu components.
|
||||
- **[sidepanel](src/sidepanel)**: Implements the side panel for details and
|
||||
configuration.
|
||||
- **[translations](src/translations)**: Holds localization JSON files.
|
||||
- **[util](src/util)**: Common utilities for dates, analytics, and data
|
||||
processing.
|
||||
- **[public](public)**: Contains static assets (like the favicon) served
|
||||
directly at the root path.
|
||||
- **[tests](tests)**: Contains end-to-end tests.
|
||||
- **[tests/probers](tests/probers)**: Live smoke tests (probers) that run
|
||||
against deployed URLs and external services.
|
||||
|
||||
142
README.md
142
README.md
@@ -17,18 +17,19 @@ If you find this project useful, consider buying me a coffee.
|
||||
[](https://www.buymeacoffee.com/pewu)
|
||||
|
||||
## Features
|
||||
* Hourglass chart
|
||||
* All relatives chart
|
||||
* Click on a person to focus
|
||||
* Open standard GEDCOM files you can export from any genealogy application
|
||||
* Load from URL (just point to a GEDCOM file on the Web)
|
||||
* Privacy – your files do not leave your computer
|
||||
* Print the whole genealogy tree
|
||||
* Export to PDF, PNG, SVG
|
||||
* Side panel with details
|
||||
* Configuration options
|
||||
* Permalinks when loading from URL
|
||||
* Cool transition animations
|
||||
|
||||
- Hourglass chart
|
||||
- All relatives chart
|
||||
- Click on a person to focus
|
||||
- Open standard GEDCOM files you can export from any genealogy application
|
||||
- Load from URL (just point to a GEDCOM file on the Web)
|
||||
- Privacy – your files do not leave your computer
|
||||
- Print the whole genealogy tree
|
||||
- Export to PDF, PNG, SVG
|
||||
- Side panel with details
|
||||
- Configuration options
|
||||
- Permalinks when loading from URL
|
||||
- Cool transition animations
|
||||
|
||||
[Changelog](CHANGELOG.md)
|
||||
|
||||
@@ -36,62 +37,82 @@ If you find this project useful, consider buying me a coffee.
|
||||
|
||||
Here is an example from the Web:
|
||||
|
||||
* [J. F. Kennedy](https://pewu.github.io/topola-viewer/#/view?url=https%3A%2F%2Fchronoplexsoftware.com%2Fmyfamilytree%2Fsamples%2FThe%2520Kennedy%2520Family.gdz) (from [chronoplexsoftware.com](https://chronoplexsoftware.com/myfamilytree/samples/))
|
||||
* [Shakespeare](https://pewu.github.io/topola-viewer/#/view?url=https%3A%2F%2Fwebtreeprint.com%2Ftp_downloader.php%3Fpath%3Dfamous_gedcoms%2Fshakespeare.ged%26file%3Dshakespeare.ged) (from [webtreeprint.com](https://webtreeprint.com/tp_famous_gedcoms.php))
|
||||
* [Maria Skłodowska-Curie](https://apps.wikitree.com/apps/wiech13/topola-viewer/#/view?indi=Sk%C5%82odowska-2&source=wikitree) (from [WikiTree](https://www.wikitree.com/wiki/Sk%C5%82odowska-2))
|
||||
- [J. F. Kennedy](https://pewu.github.io/topola-viewer/#/view?url=https%3A%2F%2Fchronoplexsoftware.com%2Fmyfamilytree%2Fsamples%2FThe%2520Kennedy%2520Family.gdz)
|
||||
(from
|
||||
[chronoplexsoftware.com](https://chronoplexsoftware.com/myfamilytree/samples/))
|
||||
- [Shakespeare](https://pewu.github.io/topola-viewer/#/view?url=https%3A%2F%2Fwebtreeprint.com%2Ftp_downloader.php%3Fpath%3Dfamous_gedcoms%2Fshakespeare.ged%26file%3Dshakespeare.ged)
|
||||
(from [webtreeprint.com](https://webtreeprint.com/tp_famous_gedcoms.php))
|
||||
- [Maria Skłodowska-Curie](https://apps.wikitree.com/apps/wiech13/topola-viewer/#/view?indi=Sk%C5%82odowska-2&source=wikitree)
|
||||
(from [WikiTree](https://www.wikitree.com/wiki/Sk%C5%82odowska-2))
|
||||
|
||||
If you have data in a genealogy database, you can export your data in GEDCOM format and load it using the "Load from file" menu.
|
||||
If you have data in a genealogy database, you can export your data in GEDCOM
|
||||
format and load it using the "Load from file" menu.
|
||||
|
||||
## Supported file formats
|
||||
|
||||
Topola Genealogy Viewer supports the following file formats:
|
||||
|
||||
* **`.ged`** – Standard GEDCOM file containing genealogical data.
|
||||
* **`.gdz`** / **`.gedzip`** / **`.zip`** – A ZIP archive that bundles a GEDCOM file together with associated media (photos, documents). This is useful when your family tree references image files and you want them to display in the chart.
|
||||
- **`.ged`** – Standard GEDCOM file containing genealogical data.
|
||||
- **`.gdz`** / **`.gedzip`** / **`.zip`** – A ZIP archive that bundles a GEDCOM
|
||||
file together with associated media (photos, documents). This is useful when
|
||||
your family tree references image files and you want them to display in the
|
||||
chart.
|
||||
|
||||
### GEDZIP archives
|
||||
|
||||
A `.gdz` file is a standard ZIP archive. When you load one, Topola Viewer automatically unzips it in the browser (your data never leaves your computer), finds the first `.ged` file inside and uses it as the genealogy data, and treats all other files as images. Image file paths are normalized (backslashes converted to forward slashes, case lowered) so they match the references in the GEDCOM.
|
||||
A `.gdz` file is a standard ZIP archive. When you load one, Topola Viewer
|
||||
automatically unzips it in the browser (your data never leaves your computer),
|
||||
finds the first `.ged` file inside and uses it as the genealogy data, and treats
|
||||
all other files as images. Image file paths are normalized (backslashes
|
||||
converted to forward slashes, case lowered) so they match the references in the
|
||||
GEDCOM.
|
||||
|
||||
To create a `.gdz` archive, place your `.ged` file and image files (e.g. in a `photos/` folder) into a directory and zip them together:
|
||||
To create a `.gdz` archive, place your `.ged` file and image files (e.g. in a
|
||||
`photos/` folder) into a directory and zip them together:
|
||||
|
||||
```bash
|
||||
zip -r family.gdz family.ged photos/
|
||||
```
|
||||
|
||||
The resulting `family.gdz` file can be loaded via "Load from file", loaded from a URL, or mounted into a Docker container (see [Docker Container Deployment](#docker-container-deployment)).
|
||||
The resulting `family.gdz` file can be loaded via "Load from file", loaded from
|
||||
a URL, or mounted into a Docker container (see
|
||||
[Docker Container Deployment](#docker-container-deployment)).
|
||||
|
||||
## Integrations
|
||||
|
||||
Topola Genealogy Viewer is being integrated into more and more Web and desktop applications.
|
||||
Here are the current integrations:
|
||||
Topola Genealogy Viewer is being integrated into more and more Web and desktop
|
||||
applications. Here are the current integrations:
|
||||
|
||||
### Gramps
|
||||
|
||||
To view your [Gramps](https://gramps-project.org/) data in Topola Genealogy Viewer,
|
||||
install [*Interactive Family Tree*](https://gramps-project.org/wiki/index.php/Interactive_Family_Tree)
|
||||
plugin from the Gramps plugin manager. The plugin will add a
|
||||
*Tools->Analysis and Exploration->Interactive Family Tree* menu item to Gramps.
|
||||
To view your [Gramps](https://gramps-project.org/) data in Topola Genealogy
|
||||
Viewer, install
|
||||
[_Interactive Family Tree_](https://gramps-project.org/wiki/index.php/Interactive_Family_Tree)
|
||||
plugin from the Gramps plugin manager. The plugin will add a _Tools->Analysis
|
||||
and Exploration->Interactive Family Tree_ menu item to Gramps.
|
||||
|
||||
Source code: https://github.com/gramps-project/addons-source/tree/master/Topola
|
||||
|
||||
### Webtrees
|
||||
|
||||
Embed Topola Genealogy Viewer in your [Webtrees](https://www.webtrees.net/) installation with the
|
||||
Embed Topola Genealogy Viewer in your [Webtrees](https://www.webtrees.net/)
|
||||
installation with the
|
||||
[Topola interactive tree addon](https://webtrees.net/download/modules#simple-auto-login---by-fanningert---20---website).
|
||||
|
||||
Source code: https://github.com/PeWu/topola-webtrees
|
||||
|
||||
### WikiTree
|
||||
|
||||
You can browse the [WikiTree](https://www.wikitree.com/) genealogy tree using Topola Genealogy Viewer.
|
||||
On a WikiTree profile page go to the *Family Tree & Tools* tab and click the *Dynamic Tree by Topola* link.
|
||||
You can browse the [WikiTree](https://www.wikitree.com/) genealogy tree using
|
||||
Topola Genealogy Viewer. On a WikiTree profile page go to the _Family Tree &
|
||||
Tools_ tab and click the _Dynamic Tree by Topola_ link.
|
||||
|
||||
Example:
|
||||
[Stephen Hawking](https://apps.wikitree.com/apps/wiech13/topola-viewer/#/view?source=wikitree&standalone=false&indi=Hawking-7)
|
||||
|
||||
Topola Genealogy Viewer is hosted on [apps.wikitree.com](https://apps.wikitree.com/apps/wiech13/topola-viewer)
|
||||
to benefit from the ability of being logged in to the WikiTree API.
|
||||
Topola Genealogy Viewer is hosted on
|
||||
[apps.wikitree.com](https://apps.wikitree.com/apps/wiech13/topola-viewer) to
|
||||
benefit from the ability of being logged in to the WikiTree API.
|
||||
|
||||
## Running locally
|
||||
|
||||
@@ -102,19 +123,25 @@ npm start
|
||||
|
||||
## Self-hosting
|
||||
|
||||
You can host Topola Genealogy Viewer on your own server. There are no specific requirements for the hosting server. There is no code that is executed on the server side. The server only hosts the application files and whole application runs in the browser.
|
||||
You can host Topola Genealogy Viewer on your own server. There are no specific
|
||||
requirements for the hosting server. There is no code that is executed on the
|
||||
server side. The server only hosts the application files and whole application
|
||||
runs in the browser.
|
||||
|
||||
You can build Topola Genealogy Viewer from source code or take a ready-to-deploy package.
|
||||
You can build Topola Genealogy Viewer from source code or take a ready-to-deploy
|
||||
package.
|
||||
|
||||
### Bulid your own
|
||||
|
||||
Here are the commands to build the application:
|
||||
|
||||
```
|
||||
git clone https://github.com/PeWu/topola-viewer.git
|
||||
cd topola-viewer
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Now, take the contents of the `dist/` folder and host it on your own server.
|
||||
|
||||
### Use an existing package
|
||||
@@ -126,21 +153,27 @@ These are the exact files that are hosted on GitHub pages.
|
||||
|
||||
### Build for your own data only
|
||||
|
||||
You can run Topola Viewer in a "single tree mode" that displays only the GEDCOM you specify. Specify the URL to a GEDCOM file in the `VITE_STATIC_URL` environment variable when building and running the application.
|
||||
You can run Topola Viewer in a "single tree mode" that displays only the GEDCOM
|
||||
you specify. Specify the URL to a GEDCOM file in the `VITE_STATIC_URL`
|
||||
environment variable when building and running the application.
|
||||
|
||||
Run locally with the specified data URL:
|
||||
|
||||
```shell
|
||||
VITE_STATIC_URL=https://example.org/sample.ged npm start
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>For Windows CMD:</summary>
|
||||
|
||||
```cmd
|
||||
set VITE_STATIC_URL=https://example.org/sample.ged && npm run build
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Build with the specified data URL:
|
||||
|
||||
```shell
|
||||
VITE_STATIC_URL=https://example.org/sample.ged npm run build
|
||||
```
|
||||
@@ -151,13 +184,15 @@ VITE_STATIC_URL=https://example.org/sample.ged npm run build
|
||||
```cmd
|
||||
set VITE_STATIC_URL=https://example.org/sample.ged && npm run build
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
The `dist/` folder will contain files that can be hosted on a Web server.
|
||||
|
||||
### Build without Google Analytics
|
||||
|
||||
Set `VITE_GOOGLE_ANALYTICS=false` to exclude Google Analytics from the build output. This will remove the external JavaScript dependency.
|
||||
Set `VITE_GOOGLE_ANALYTICS=false` to exclude Google Analytics from the build
|
||||
output. This will remove the external JavaScript dependency.
|
||||
|
||||
```shell
|
||||
VITE_GOOGLE_ANALYTICS=false npm run build
|
||||
@@ -169,27 +204,38 @@ VITE_GOOGLE_ANALYTICS=false npm run build
|
||||
```cmd
|
||||
set VITE_GOOGLE_ANALYTICS=false && npm run build
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
This may be combined with the other build environment variables described above.
|
||||
|
||||
### Alternative build
|
||||
|
||||
The [topola-webpack](https://github.com/develancer/topola-webpack) tool can build a Topola Genealogy Viewer package bundled together with a GEDCOM file.
|
||||
The [topola-webpack](https://github.com/develancer/topola-webpack) tool can
|
||||
build a Topola Genealogy Viewer package bundled together with a GEDCOM file.
|
||||
|
||||
## Docker Container Deployment
|
||||
|
||||
Topola Viewer can be run locally or deployed to standard cloud environments using Docker.
|
||||
Topola Viewer can be run locally or deployed to standard cloud environments
|
||||
using Docker.
|
||||
|
||||
### Running Topola Viewer
|
||||
|
||||
To pull and run Topola Viewer:
|
||||
|
||||
```bash
|
||||
docker run -d -p 8080:8080 ghcr.io/pewu/topola-viewer:latest
|
||||
```
|
||||
Open your web browser and go to `http://localhost:8080` to upload your family tree files locally.
|
||||
|
||||
Open your web browser and go to `http://localhost:8080` to upload your family
|
||||
tree files locally.
|
||||
|
||||
### Running with Your Own Data (Zero-Build Run)
|
||||
You can serve a standalone, pre-loaded family tree with zero compilation by mounting your family tree data (a `.ged` file or a zipped `.gdz` archive containing photos) directly into the running container:
|
||||
|
||||
You can serve a standalone, pre-loaded family tree with zero compilation by
|
||||
mounting your family tree data (a `.ged` file or a zipped `.gdz` archive
|
||||
containing photos) directly into the running container:
|
||||
|
||||
```bash
|
||||
docker run -d -p 8080:8080 \
|
||||
-e STATIC_URL=my_family.gdz \
|
||||
@@ -198,16 +244,22 @@ docker run -d -p 8080:8080 \
|
||||
```
|
||||
|
||||
### Building the Base Image Locally
|
||||
|
||||
To build the base image from source:
|
||||
|
||||
```bash
|
||||
docker build -t topola-viewer -f docker/Dockerfile .
|
||||
```
|
||||
|
||||
### Ready-To-Use Standalone Templates
|
||||
For creating completely self-contained Docker images that bundle your genealogy data and serve it instantly, see these pre-configured examples:
|
||||
|
||||
1. **[Simple Standalone Tree](docker/examples/simple/)**: Demonstrates how to package and pre-load a `.ged` file directly inside a custom image.
|
||||
2. **[Standalone Tree with Photos](docker/examples/photos/)**: Packages your family tree and a `photos/` folder into a valid `.gdz` archive on-the-fly.
|
||||
For creating completely self-contained Docker images that bundle your genealogy
|
||||
data and serve it instantly, see these pre-configured examples:
|
||||
|
||||
1. **[Simple Standalone Tree](docker/examples/simple/)**: Demonstrates how to
|
||||
package and pre-load a `.ged` file directly inside a custom image.
|
||||
2. **[Standalone Tree with Photos](docker/examples/photos/)**: Packages your
|
||||
family tree and a `photos/` folder into a valid `.gdz` archive on-the-fly.
|
||||
|
||||
## Additional options
|
||||
|
||||
@@ -217,4 +269,6 @@ Add `&handleCors=false` to the URL to avoid using the CORS proxy
|
||||
|
||||
### `embedded`
|
||||
|
||||
Add `&embedded=true` to the URL. This option removes the options to open a different file. It is an option that was intended to be used when Topola Genealogy Viewer is in an iframe.
|
||||
Add `&embedded=true` to the URL. This option removes the options to open a
|
||||
different file. It is an option that was intended to be used when Topola
|
||||
Genealogy Viewer is in an iframe.
|
||||
|
||||
@@ -1,118 +1,303 @@
|
||||
# Refactoring App.tsx Design Document
|
||||
|
||||
## Problem Description
|
||||
The main application component, [App](../src/app.tsx), has grown into a large and complex monolith that coordinates URL routing, query parameter synchronization, asynchronous data fetching, UI layout rendering, and external integrations like the WebMCP bridge. Because these distinct responsibilities are combined within a single file, the codebase has become difficult to navigate, test, and safely modify without introducing unintended regressions. This refactoring effort aims to systematically decouple these concerns by decomposing the monolith into small, single-responsibility modules, separating the page-level UI layouts, and establishing the URL query parameters as the single source of truth for viewer state.
|
||||
|
||||
The main application component, [App](../src/app.tsx), has grown into a large
|
||||
and complex monolith that coordinates URL routing, query parameter
|
||||
synchronization, asynchronous data fetching, UI layout rendering, and external
|
||||
integrations like the WebMCP bridge. Because these distinct responsibilities are
|
||||
combined within a single file, the codebase has become difficult to navigate,
|
||||
test, and safely modify without introducing unintended regressions. This
|
||||
refactoring effort aims to systematically decouple these concerns by decomposing
|
||||
the monolith into small, single-responsibility modules, separating the
|
||||
page-level UI layouts, and establishing the URL query parameters as the single
|
||||
source of truth for viewer state.
|
||||
|
||||
## The Technical Plan
|
||||
|
||||
At a high level, the refactored architecture splits user interface representation from business logic and external connections. Instead of a single central orchestrator holding all states, the app relies on the browser's URL to drive the active view and delegates sub-features (like data fetching, authentication, and external syncing) to dedicated hooks and services.
|
||||
At a high level, the refactored architecture splits user interface
|
||||
representation from business logic and external connections. Instead of a single
|
||||
central orchestrator holding all states, the app relies on the browser's URL to
|
||||
drive the active view and delegates sub-features (like data fetching,
|
||||
authentication, and external syncing) to dedicated hooks and services.
|
||||
|
||||
### Explanatory Overview
|
||||
|
||||
* **Routing & Page Views:** The root [App](../src/app.tsx) component will serve purely as a routing switch, but will retain the Google Drive "Open with" flow redirection logic. It decides whether to display the landing screen (`IntroPage`) or the visual family tree explorer (`ViewPage`). It is also responsible for stripping external authorization query parameters (like `state` and `authcode`) from the browser window's URL.
|
||||
* **Layout Nesting (TopBar & SidePanel):** Rather than having a global layout shell wrapping all pages, each page will compose its own layout explicitly. This allows the `ViewPage` to pass live chart selection data, configuration, and export triggers directly to its own rendered `TopBar`, while the `IntroPage` remains clean and completely decoupled from viewer states. To prevent locking the user interface on loading errors, `ViewPage` will render the `TopBar` even when in loading or error states.
|
||||
* **URL as Single Source of Truth (SSOT):** Rather than keeping user settings (like the selected person, active chart view, and display preferences) in temporary React memory and continuously syncing them to the URL, the application will read these properties directly from the URL query string on every render. This includes the detail selection sidebar state (`detailIndi` will map to `?detail=...`). Changing a setting now simply updates the browser's URL, and React automatically updates the view.
|
||||
* **Data Service Layer:** All logic involved in downloading, caching, and parsing GEDCOM or WikiTree files from different sources is fully isolated from the user interface, triggered via a unified API spec on the `ViewPage`.
|
||||
* **WebMCP & Auth Hooks:** Third-party integrations and security layers (like the WebMCP developer bridge and Google Drive sign-in states) are extracted into custom React hooks. These hooks manage side effects independently, removing clutter from the layout rendering code.
|
||||
- **Routing & Page Views:** The root [App](../src/app.tsx) component will serve
|
||||
purely as a routing switch, but will retain the Google Drive "Open with" flow
|
||||
redirection logic. It decides whether to display the landing screen
|
||||
(`IntroPage`) or the visual family tree explorer (`ViewPage`). It is also
|
||||
responsible for stripping external authorization query parameters (like
|
||||
`state` and `authcode`) from the browser window's URL.
|
||||
- **Layout Nesting (TopBar & SidePanel):** Rather than having a global layout
|
||||
shell wrapping all pages, each page will compose its own layout explicitly.
|
||||
This allows the `ViewPage` to pass live chart selection data, configuration,
|
||||
and export triggers directly to its own rendered `TopBar`, while the
|
||||
`IntroPage` remains clean and completely decoupled from viewer states. To
|
||||
prevent locking the user interface on loading errors, `ViewPage` will render
|
||||
the `TopBar` even when in loading or error states.
|
||||
- **URL as Single Source of Truth (SSOT):** Rather than keeping user settings
|
||||
(like the selected person, active chart view, and display preferences) in
|
||||
temporary React memory and continuously syncing them to the URL, the
|
||||
application will read these properties directly from the URL query string on
|
||||
every render. This includes the detail selection sidebar state (`detailIndi`
|
||||
will map to `?detail=...`). Changing a setting now simply updates the
|
||||
browser's URL, and React automatically updates the view.
|
||||
- **Data Service Layer:** All logic involved in downloading, caching, and
|
||||
parsing GEDCOM or WikiTree files from different sources is fully isolated from
|
||||
the user interface, triggered via a unified API spec on the `ViewPage`.
|
||||
- **WebMCP & Auth Hooks:** Third-party integrations and security layers (like
|
||||
the WebMCP developer bridge and Google Drive sign-in states) are extracted
|
||||
into custom React hooks. These hooks manage side effects independently,
|
||||
removing clutter from the layout rendering code.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 1. Global State / Prop Drilling in Root Component
|
||||
* **Approach:** Keep all the viewer and configuration state inside the root `App` component and pass them down as props to the nested components (such as `IntroPage` and `ViewPage`).
|
||||
* **Why Rejected:** This would fail to address the core problem. The root `app.tsx` file would remain a massive state-managing monolith containing dozens of hooks and effects, making it just as hard to maintain and test as before.
|
||||
|
||||
- **Approach:** Keep all the viewer and configuration state inside the root
|
||||
`App` component and pass them down as props to the nested components (such as
|
||||
`IntroPage` and `ViewPage`).
|
||||
- **Why Rejected:** This would fail to address the core problem. The root
|
||||
`app.tsx` file would remain a massive state-managing monolith containing
|
||||
dozens of hooks and effects, making it just as hard to maintain and test as
|
||||
before.
|
||||
|
||||
### 2. React Context for Viewer Session
|
||||
* **Approach:** Create a global React Context (e.g., `GenealogySessionContext`) that holds the active viewer's data, parameters, and callback handlers, and wrap the application root in a Context Provider.
|
||||
* **Why Rejected:** While a valid pattern, introducing Context adds boilerplate and can lead to unnecessary re-renders. Explicit page-level nesting—where `ViewPage` and `IntroPage` each explicitly render their own `TopBar` headers—is a simpler and more isolated design. It allows components to consume direct, local props without global state providers.
|
||||
|
||||
- **Approach:** Create a global React Context (e.g., `GenealogySessionContext`)
|
||||
that holds the active viewer's data, parameters, and callback handlers, and
|
||||
wrap the application root in a Context Provider.
|
||||
- **Why Rejected:** While a valid pattern, introducing Context adds boilerplate
|
||||
and can lead to unnecessary re-renders. Explicit page-level nesting—where
|
||||
`ViewPage` and `IntroPage` each explicitly render their own `TopBar`
|
||||
headers—is a simpler and more isolated design. It allows components to consume
|
||||
direct, local props without global state providers.
|
||||
|
||||
### 3. React-State-to-URL Syncing (Bidirectional Sync)
|
||||
* **Approach:** Maintain React state variables (`useState`) for the selected individual, chart view type, and configuration, and keep them synchronized with the URL query parameters using `useEffect` loops.
|
||||
* **Why Rejected:** Bidirectional synchronization is extremely error-prone and a frequent cause of infinite rendering loops and race conditions (especially with async network requests). Transitioning to the **URL as the Single Source of Truth** (deriving all active states directly from the URL on render) completely removes the sync hooks and ensures browser back/forward buttons work correctly out of the box.
|
||||
|
||||
- **Approach:** Maintain React state variables (`useState`) for the selected
|
||||
individual, chart view type, and configuration, and keep them synchronized
|
||||
with the URL query parameters using `useEffect` loops.
|
||||
- **Why Rejected:** Bidirectional synchronization is extremely error-prone and a
|
||||
frequent cause of infinite rendering loops and race conditions (especially
|
||||
with async network requests). Transitioning to the **URL as the Single Source
|
||||
of Truth** (deriving all active states directly from the URL on render)
|
||||
completely removes the sync hooks and ensures browser back/forward buttons
|
||||
work correctly out of the box.
|
||||
|
||||
### 4. Component-First Refactoring (Extracting Visual Leaf Nodes)
|
||||
* **Approach:** Start by extracting pure UI layout leaf nodes (such as the chart display wrappers or sidebar layouts) before addressing the business logic.
|
||||
* **Why Rejected:** The primary complexity in `app.tsx` lies in the lifecycle management and side effects, not the JSX markup size. Starting with leaf components leaves the complex state orchestrations untouched. Instead, we prioritized extracting utility modules, isolated background hooks (like `WebMcpBridge`), and transitioning states to the URL before separating UI pages.
|
||||
|
||||
- **Approach:** Start by extracting pure UI layout leaf nodes (such as the chart
|
||||
display wrappers or sidebar layouts) before addressing the business logic.
|
||||
- **Why Rejected:** The primary complexity in `app.tsx` lies in the lifecycle
|
||||
management and side effects, not the JSX markup size. Starting with leaf
|
||||
components leaves the complex state orchestrations untouched. Instead, we
|
||||
prioritized extracting utility modules, isolated background hooks (like
|
||||
`WebMcpBridge`), and transitioning states to the URL before separating UI
|
||||
pages.
|
||||
|
||||
## Detailed Implementation
|
||||
|
||||
To refactor `app.tsx` safely and maintain continuous code correctness, we will modify one file and create eight new ones. This section maps out the role of each file and the step-by-step implementation plan.
|
||||
To refactor `app.tsx` safely and maintain continuous code correctness, we will
|
||||
modify one file and create eight new ones. This section maps out the role of
|
||||
each file and the step-by-step implementation plan.
|
||||
|
||||
### Affected Files and Rationale
|
||||
|
||||
#### 1. [MODIFY] `src/app.tsx`
|
||||
* **Role:** Will be stripped down to a lightweight Router component that only defines page-level paths, coordinates Google Drive "Open with" redirection synchronously on render, and renders `<IntroPage />` or `<ViewPage />`.
|
||||
* **Rationale:** Eliminates the monolithic coordinator by delegating view orchestration, state synchronization, and page rendering to dedicated page components and hooks, while retaining initial payload interception.
|
||||
|
||||
- **Role:** Will be stripped down to a lightweight Router component that only
|
||||
defines page-level paths, coordinates Google Drive "Open with" redirection
|
||||
synchronously on render, and renders `<IntroPage />` or `<ViewPage />`.
|
||||
- **Rationale:** Eliminates the monolithic coordinator by delegating view
|
||||
orchestration, state synchronization, and page rendering to dedicated page
|
||||
components and hooks, while retaining initial payload interception.
|
||||
|
||||
#### 2. [NEW] `src/components/error_display.tsx`
|
||||
* **Role:** Will contain the presentational components `ErrorMessage` and `ErrorPopup`.
|
||||
* **Rationale:** Keeps simple UI leaf components separated from the main business logic and state coordinates.
|
||||
|
||||
- **Role:** Will contain the presentational components `ErrorMessage` and
|
||||
`ErrorPopup`.
|
||||
- **Rationale:** Keeps simple UI leaf components separated from the main
|
||||
business logic and state coordinates.
|
||||
|
||||
#### 3. [NEW] `src/datasource/instances.ts`
|
||||
* **Role:** Declares and exports global datasource class instances (`uploadedDataSource`, `gedcomUrlDataSource`, `embeddedDataSource`, `googleDriveDataSource`).
|
||||
* **Rationale:** Avoids declaring stateful data service instances inside layout and view components, improving separation of concerns.
|
||||
|
||||
- **Role:** Declares and exports global datasource class instances
|
||||
(`uploadedDataSource`, `gedcomUrlDataSource`, `embeddedDataSource`,
|
||||
`googleDriveDataSource`).
|
||||
- **Rationale:** Avoids declaring stateful data service instances inside layout
|
||||
and view components, improving separation of concerns.
|
||||
|
||||
#### 4. [NEW] `src/util/url_args.ts`
|
||||
* **Role:** Exports `getStaticUrl`, `getParamFromSearch`, and `getArguments` utility functions, along with their related TypeScript interfaces (`Arguments`, `DataSourceSpec`), including support for the `detail` query parameter. It also provides a pure helper function (`getUrlForArgs`) that accepts current/new arguments and returns a path/query object suitable for passing to React Router's `navigate` function.
|
||||
* **Rationale:** Moves parsing and decoding logic for query strings out of the React components, allowing this code to be tested independently in isolation.
|
||||
|
||||
- **Role:** Exports `getStaticUrl`, `getParamFromSearch`, and `getArguments`
|
||||
utility functions, along with their related TypeScript interfaces
|
||||
(`Arguments`, `DataSourceSpec`), including support for the `detail` query
|
||||
parameter. It also provides a pure helper function (`getUrlForArgs`) that
|
||||
accepts current/new arguments and returns a path/query object suitable for
|
||||
passing to React Router's `navigate` function.
|
||||
- **Rationale:** Moves parsing and decoding logic for query strings out of the
|
||||
React components, allowing this code to be tested independently in isolation.
|
||||
|
||||
#### 5. [NEW] `src/util/url_args.spec.ts`
|
||||
* **Role:** Jest unit tests verifying the correctness of `getArguments` under different query configurations (e.g. WikiTree authcodes, Google Drive file IDs, static URLs, local uploads).
|
||||
* **Rationale:** URL parsing acts as the gateway to all loader code; ensuring this logic is 100% correct via unit testing prevents regressions during URL SSOT changes.
|
||||
|
||||
- **Role:** Jest unit tests verifying the correctness of `getArguments` under
|
||||
different query configurations (e.g. WikiTree authcodes, Google Drive file
|
||||
IDs, static URLs, local uploads).
|
||||
- **Rationale:** URL parsing acts as the gateway to all loader code; ensuring
|
||||
this logic is 100% correct via unit testing prevents regressions during URL
|
||||
SSOT changes.
|
||||
|
||||
#### 6. [NEW] `src/hooks/use_webmcp_bridge.ts`
|
||||
* **Role:** Custom React hook `useWebMcpBridge(data, detailIndi, onSelection)` called in `ViewPage` encapsulating the WebMCP bridge creation, tool registration, state synchronization, and cleanup effects.
|
||||
* **Rationale:** Isolates external developer tools syncing from the UI components.
|
||||
|
||||
- **Role:** Custom React hook `useWebMcpBridge(data, detailIndi, onSelection)`
|
||||
called in `ViewPage` encapsulating the WebMCP bridge creation, tool
|
||||
registration, state synchronization, and cleanup effects.
|
||||
- **Rationale:** Isolates external developer tools syncing from the UI
|
||||
components.
|
||||
|
||||
#### 7. [NEW] `src/hooks/use_google_auth.ts`
|
||||
* **Role:** Custom React hook `useGoogleAuth()` encapsulating access to the global `googleDriveService` authentication token, session storage cache clearing, and login/logout trigger states.
|
||||
* **Rationale:** Provides shared authentication state that can be consumed by the `TopBar` headers of both page components without needing parent state lifting.
|
||||
|
||||
- **Role:** Custom React hook `useGoogleAuth()` encapsulating access to the
|
||||
global `googleDriveService` authentication token, session storage cache
|
||||
clearing, and login/logout trigger states.
|
||||
- **Rationale:** Provides shared authentication state that can be consumed by
|
||||
the `TopBar` headers of both page components without needing parent state
|
||||
lifting.
|
||||
|
||||
#### 8. [NEW] `src/pages/view_page.tsx`
|
||||
* **Role:** The page view orchestrating the chart visualizer workspace. It handles asynchronous loader triggers, keeps local data loading states, derives view configurations from the URL, and renders the Sidebar, SidePanel, and its own explicit `TopBar` (including rendering on loading/error states). It also hosts the state and rendering for `<GoogleAuthModal>` to handle Google Drive auth errors caught during data loading, handles memoization of `WikiTreeDataSource` (due to its react-intl context requirement), performs Object URL cleanup on unmount, and runs `updateChartWithConfig` synchronously during the render pass to synchronize URL-derived configuration settings with the in-memory mutated chart data.
|
||||
* **Rationale:** Consolidates the chart workspace state and render tree away from the root router component.
|
||||
|
||||
- **Role:** The page view orchestrating the chart visualizer workspace. It
|
||||
handles asynchronous loader triggers, keeps local data loading states, derives
|
||||
view configurations from the URL, and renders the Sidebar, SidePanel, and its
|
||||
own explicit `TopBar` (including rendering on loading/error states). It also
|
||||
hosts the state and rendering for `<GoogleAuthModal>` to handle Google Drive
|
||||
auth errors caught during data loading, handles memoization of
|
||||
`WikiTreeDataSource` (due to its react-intl context requirement), performs
|
||||
Object URL cleanup on unmount, and runs `updateChartWithConfig` synchronously
|
||||
during the render pass to synchronize URL-derived configuration settings with
|
||||
the in-memory mutated chart data.
|
||||
- **Rationale:** Consolidates the chart workspace state and render tree away
|
||||
from the root router component.
|
||||
|
||||
#### 9. [NEW] `src/pages/intro_page.tsx`
|
||||
* **Role:** The landing page component wrapping the `Intro` presentation and rendering its own simple `TopBar`.
|
||||
* **Rationale:** Decouples the landing screen structure from the chart visualizer's stateful layout shell.
|
||||
|
||||
- **Role:** The landing page component wrapping the `Intro` presentation and
|
||||
rendering its own simple `TopBar`.
|
||||
- **Rationale:** Decouples the landing screen structure from the chart
|
||||
visualizer's stateful layout shell.
|
||||
|
||||
---
|
||||
|
||||
### Step-by-Step Execution Plan
|
||||
|
||||
#### Phase 1: Pure Component and Utility Extraction
|
||||
* [x] **Step 1.1:** Create `src/components/error_display.tsx` and move `ErrorMessage` and `ErrorPopup` from `src/app.tsx`. Update imports in `src/app.tsx`.
|
||||
* [x] **Step 1.2:** Create `src/datasource/instances.ts` and move data source class instantiations. Update references in `src/app.tsx`. Refactor `EmbeddedDataSource` to clean up its message event listener when the loading promise resolves or rejects (or track listener state) to prevent duplicate event listener leaks on multiple page mounts.
|
||||
* [x] **Step 1.3:** Create `src/util/url_args.ts` (the parsing utility) and `src/util/url_args.spec.ts` (its Jest unit test suite) together. Extract URL query parameter parsing functions and types from `src/app.tsx`, write comprehensive tests, update imports in `src/app.tsx`, and run `npm test` to verify.
|
||||
* [x] **Step 1.4:** Modify `src/menu/top_bar.tsx` to make chart-specific props and event handlers optional (e.g. `data`, `allowAllRelativesChart`, `allowPrintAndDownload`, `eventHandlers`, etc.), preparing the component for rendering on the landing screen without dummy properties.
|
||||
|
||||
- [x] **Step 1.1:** Create `src/components/error_display.tsx` and move
|
||||
`ErrorMessage` and `ErrorPopup` from `src/app.tsx`. Update imports in
|
||||
`src/app.tsx`.
|
||||
- [x] **Step 1.2:** Create `src/datasource/instances.ts` and move data source
|
||||
class instantiations. Update references in `src/app.tsx`. Refactor
|
||||
`EmbeddedDataSource` to clean up its message event listener when the
|
||||
loading promise resolves or rejects (or track listener state) to prevent
|
||||
duplicate event listener leaks on multiple page mounts.
|
||||
- [x] **Step 1.3:** Create `src/util/url_args.ts` (the parsing utility) and
|
||||
`src/util/url_args.spec.ts` (its Jest unit test suite) together. Extract
|
||||
URL query parameter parsing functions and types from `src/app.tsx`, write
|
||||
comprehensive tests, update imports in `src/app.tsx`, and run `npm test`
|
||||
to verify.
|
||||
- [x] **Step 1.4:** Modify `src/menu/top_bar.tsx` to make chart-specific props
|
||||
and event handlers optional (e.g. `data`, `allowAllRelativesChart`,
|
||||
`allowPrintAndDownload`, `eventHandlers`, etc.), preparing the component
|
||||
for rendering on the landing screen without dummy properties.
|
||||
|
||||
#### Phase 2: WebMCP Bridge Extraction
|
||||
* [x] **Step 2.1:** Create `src/hooks/use_webmcp_bridge.ts` wrapping WebMCP registration and synchronization effects.
|
||||
* [x] **Step 2.2:** Replace the inline WebMCP logic and `useEffect` blocks in `src/app.tsx` with a single call to the custom `useWebMcpBridge` hook. Verify using `npm test`. Note: During Phase 4, this hook call will be moved from `src/app.tsx` into `src/pages/view_page.tsx` where the layout state is relocated.
|
||||
|
||||
- [x] **Step 2.1:** Create `src/hooks/use_webmcp_bridge.ts` wrapping WebMCP
|
||||
registration and synchronization effects.
|
||||
- [x] **Step 2.2:** Replace the inline WebMCP logic and `useEffect` blocks in
|
||||
`src/app.tsx` with a single call to the custom `useWebMcpBridge` hook.
|
||||
Verify using `npm test`. Note: During Phase 4, this hook call will be
|
||||
moved from `src/app.tsx` into `src/pages/view_page.tsx` where the layout
|
||||
state is relocated.
|
||||
|
||||
#### Phase 3: Shifting to URL as Single Source of Truth
|
||||
We will gradually eliminate React state variables in `src/app.tsx` and derive them (including settings like `standalone`, `showWikiTreeMenus`, and `freezeAnimation`) directly from the `useLocation()` search query parameters on render:
|
||||
* [x] **Step 3.1a:** Shift `chartType` to URL as SSOT. Derive it using `useMemo` from search parameters, replace calls to `setChartType` with URL updates, and remove the React state variable and sync effect.
|
||||
* [x] **Step 3.1b:** Shift `standalone` to URL as SSOT. Derive it directly from search parameters on render and remove its React state variable.
|
||||
* [x] **Step 3.1c:** Shift `showWikiTreeMenus` to URL as SSOT. Derive it directly from search parameters on render and remove its React state variable.
|
||||
* [x] **Step 3.1d:** Shift `freezeAnimation` to URL as SSOT. Derive it directly from search parameters on render and remove its React state variable.
|
||||
* [x] **Step 3.2:** Extract `selection` and `detailIndi` state. Parse them directly from URL params (`indi`, `gen`, and `detail`); update display selectors. Remove React states. Ensure that chart selection changes (`onSelection` callback) explicitly clear or update the `detail` query parameter to match the new selection to avoid getting stuck on the old details viewport. Also update the detail-only selection handler (`onDetailSelection`) to update the `detail` query parameter in the URL.
|
||||
* [x] **Step 3.3:** Extract `showSidePanel` state. Derive state directly from `?sidePanel=` parameter. Remove React state. Enhance URL args helpers to allow generating path/query target objects with replaced values, and ensure layout settings (`sidePanel`) and configuration changes use `replace: true` to prevent polluting the browser history stack.
|
||||
* [x] **Step 3.4:** Extract `config` state. Parse display settings on render using `argsToConfig` helper. Remove state. In `ViewPage`, run `updateChartWithConfig(config, data)` synchronously during the render pass (e.g. in the `useMemo` that derives query parameters/config) to update the in-memory chart data before it is rendered by the `<Chart>` child component. Run E2E and visual tests to verify no rendering regressions occurred.
|
||||
|
||||
We will gradually eliminate React state variables in `src/app.tsx` and derive
|
||||
them (including settings like `standalone`, `showWikiTreeMenus`, and
|
||||
`freezeAnimation`) directly from the `useLocation()` search query parameters on
|
||||
render:
|
||||
|
||||
- [x] **Step 3.1a:** Shift `chartType` to URL as SSOT. Derive it using `useMemo`
|
||||
from search parameters, replace calls to `setChartType` with URL updates,
|
||||
and remove the React state variable and sync effect.
|
||||
- [x] **Step 3.1b:** Shift `standalone` to URL as SSOT. Derive it directly from
|
||||
search parameters on render and remove its React state variable.
|
||||
- [x] **Step 3.1c:** Shift `showWikiTreeMenus` to URL as SSOT. Derive it
|
||||
directly from search parameters on render and remove its React state
|
||||
variable.
|
||||
- [x] **Step 3.1d:** Shift `freezeAnimation` to URL as SSOT. Derive it directly
|
||||
from search parameters on render and remove its React state variable.
|
||||
- [x] **Step 3.2:** Extract `selection` and `detailIndi` state. Parse them
|
||||
directly from URL params (`indi`, `gen`, and `detail`); update display
|
||||
selectors. Remove React states. Ensure that chart selection changes
|
||||
(`onSelection` callback) explicitly clear or update the `detail` query
|
||||
parameter to match the new selection to avoid getting stuck on the old
|
||||
details viewport. Also update the detail-only selection handler
|
||||
(`onDetailSelection`) to update the `detail` query parameter in the URL.
|
||||
- [x] **Step 3.3:** Extract `showSidePanel` state. Derive state directly from
|
||||
`?sidePanel=` parameter. Remove React state. Enhance URL args helpers to
|
||||
allow generating path/query target objects with replaced values, and
|
||||
ensure layout settings (`sidePanel`) and configuration changes use
|
||||
`replace: true` to prevent polluting the browser history stack.
|
||||
- [x] **Step 3.4:** Extract `config` state. Parse display settings on render
|
||||
using `argsToConfig` helper. Remove state. In `ViewPage`, run
|
||||
`updateChartWithConfig(config, data)` synchronously during the render pass
|
||||
(e.g. in the `useMemo` that derives query parameters/config) to update the
|
||||
in-memory chart data before it is rendered by the `<Chart>` child
|
||||
component. Run E2E and visual tests to verify no rendering regressions
|
||||
occurred.
|
||||
|
||||
#### Phase 4: Structural Page Nesting
|
||||
* [x] **Step 4.1:** Create `src/hooks/use_google_auth.ts` to manage Google Drive authentication token updates. Remove `hasGoogleToken` state from `src/app.tsx`.
|
||||
* [x] **Step 4.2:** Create `src/pages/view_page.tsx` containing layout rendering (`renderMainArea`), asynchronous data loaders, view state management, `revokeObjectUrls` cleanups, and `<GoogleAuthModal>` error fallback state and rendering.
|
||||
* [x] **Step 4.3:** Create `src/pages/intro_page.tsx` rendering the landing screen and its own local header.
|
||||
* [x] **Step 4.4:** Refactor `src/app.tsx` to serve as a pure routing switch routing to `<IntroPage />` or `<ViewPage />`. In `App`, handle Google Drive redirection synchronously during the render pass by returning a `<Navigate replace />` element (preventing render flashing), and ensure the redirection parser checks both the router `location.search` and the external `window.location.search` to handle HashRouter query structure robustly. Once external parameters (like `state` or `authcode`) are detected, they must be stripped from `window.location.search` using `window.history.replaceState` to prevent redirection loops on page refresh. Remove all layout state logic from the root file, including moving the `useWebMcpBridge` hook invocation into `<ViewPage />`. Run final verification scripts (`npm run check:all`).
|
||||
|
||||
- [x] **Step 4.1:** Create `src/hooks/use_google_auth.ts` to manage Google Drive
|
||||
authentication token updates. Remove `hasGoogleToken` state from
|
||||
`src/app.tsx`.
|
||||
- [x] **Step 4.2:** Create `src/pages/view_page.tsx` containing layout rendering
|
||||
(`renderMainArea`), asynchronous data loaders, view state management,
|
||||
`revokeObjectUrls` cleanups, and `<GoogleAuthModal>` error fallback state
|
||||
and rendering.
|
||||
- [x] **Step 4.3:** Create `src/pages/intro_page.tsx` rendering the landing
|
||||
screen and its own local header.
|
||||
- [x] **Step 4.4:** Refactor `src/app.tsx` to serve as a pure routing switch
|
||||
routing to `<IntroPage />` or `<ViewPage />`. In `App`, handle Google
|
||||
Drive redirection synchronously during the render pass by returning a
|
||||
`<Navigate replace />` element (preventing render flashing), and ensure
|
||||
the redirection parser checks both the router `location.search` and the
|
||||
external `window.location.search` to handle HashRouter query structure
|
||||
robustly. Once external parameters (like `state` or `authcode`) are
|
||||
detected, they must be stripped from `window.location.search` using
|
||||
`window.history.replaceState` to prevent redirection loops on page
|
||||
refresh. Remove all layout state logic from the root file, including
|
||||
moving the `useWebMcpBridge` hook invocation into `<ViewPage />`. Run
|
||||
final verification scripts (`npm run check:all`).
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### 1. Image Object URL Memory Management
|
||||
* Instead of managing image revocation inside the component lifecycle (which can break back-navigation since the `ViewPage` unmounts and revokes URLs), we will explore having the global datasource singletons in `instances.ts` (such as `UploadedDataSource` and `GoogleDriveDataSource`) manage the lifetime of their image URL maps. When `loadData` is called on a datasource, it will check if it has a reference to a previous image map and explicitly revoke it before loading the new dataset.
|
||||
|
||||
- Instead of managing image revocation inside the component lifecycle (which can
|
||||
break back-navigation since the `ViewPage` unmounts and revokes URLs), we will
|
||||
explore having the global datasource singletons in `instances.ts` (such as
|
||||
`UploadedDataSource` and `GoogleDriveDataSource`) manage the lifetime of their
|
||||
image URL maps. When `loadData` is called on a datasource, it will check if it
|
||||
has a reference to a previous image map and explicitly revoke it before
|
||||
loading the new dataset.
|
||||
|
||||
### 2. TopBar Layout Refactoring
|
||||
* Refactor `top_bar.tsx` to make all chart-specific event handlers and properties optional, or split the header layout from the stateful workspace menus. This will eliminate passing dummy or empty handlers from the `IntroPage`.
|
||||
|
||||
|
||||
|
||||
|
||||
- Refactor `top_bar.tsx` to make all chart-specific event handlers and
|
||||
properties optional, or split the header layout from the stateful workspace
|
||||
menus. This will eliminate passing dummy or empty handlers from the
|
||||
`IntroPage`.
|
||||
|
||||
@@ -2,79 +2,164 @@
|
||||
|
||||
## 1. Problem Description
|
||||
|
||||
Topola Viewer is a modern, client-side web application designed for exploring and visualizing genealogy data from GEDCOM files. Currently, running a custom instance with a pre-loaded family tree and photos requires manual local development builds or setting up complex web hosting environments. This project aims to package Topola Viewer inside an ultra-lightweight, secure, and production-ready Docker container. This container will allow users to instantly deploy a vanilla viewer or easily serve their own private, self-contained family trees with zero local compilation or build-tool dependencies.
|
||||
Topola Viewer is a modern, client-side web application designed for exploring
|
||||
and visualizing genealogy data from GEDCOM files. Currently, running a custom
|
||||
instance with a pre-loaded family tree and photos requires manual local
|
||||
development builds or setting up complex web hosting environments. This project
|
||||
aims to package Topola Viewer inside an ultra-lightweight, secure, and
|
||||
production-ready Docker container. This container will allow users to instantly
|
||||
deploy a vanilla viewer or easily serve their own private, self-contained family
|
||||
trees with zero local compilation or build-tool dependencies.
|
||||
|
||||
## 2. Major Components & How They Interact
|
||||
|
||||
At a high level, the containerized Topola Viewer consists of three primary components working together to serve and display family trees:
|
||||
At a high level, the containerized Topola Viewer consists of three primary
|
||||
components working together to serve and display family trees:
|
||||
|
||||
1. **The User's Web Browser (The App)**: This is where Topola Viewer actually runs. Since it is a single-page React application, the browser downloads the application files once and executes all the logic, chart rendering, and user interactions locally on the user's computer.
|
||||
2. **The Caddy Web Server (The Helper)**: This is a lightweight, secure, and fast background server running inside the Docker container. Its job is to serve the application files (HTML, CSS, JavaScript) and the family tree package when the browser requests them. Additionally, Caddy dynamically updates the main HTML page on-the-fly when it is loaded, injecting the path or address of the family tree file specified by the container's configuration.
|
||||
3. **The Genealogy Data (The Data)**: This is the family tree description file. Depending on whether you have local photo assets:
|
||||
* **Unzipped File (`.ged`)**: If you do not have local photos, you can serve your standard, plain-text `.ged` file directly.
|
||||
* **Zipped Archive (`.gdz` or `.zip`)**: If you want to bundle family photos, you compress the `.ged` file and the photos together into a single archive. Topola Viewer will automatically unzip and map the photos to the tree.
|
||||
Serving this as a single file (either `.ged` or `.gdz`) keeps the container setup simple and highly portable.
|
||||
1. **The User's Web Browser (The App)**: This is where Topola Viewer actually
|
||||
runs. Since it is a single-page React application, the browser downloads the
|
||||
application files once and executes all the logic, chart rendering, and user
|
||||
interactions locally on the user's computer.
|
||||
2. **The Caddy Web Server (The Helper)**: This is a lightweight, secure, and
|
||||
fast background server running inside the Docker container. Its job is to
|
||||
serve the application files (HTML, CSS, JavaScript) and the family tree
|
||||
package when the browser requests them. Additionally, Caddy dynamically
|
||||
updates the main HTML page on-the-fly when it is loaded, injecting the path
|
||||
or address of the family tree file specified by the container's
|
||||
configuration.
|
||||
3. **The Genealogy Data (The Data)**: This is the family tree description file.
|
||||
Depending on whether you have local photo assets:
|
||||
- **Unzipped File (`.ged`)**: If you do not have local photos, you can serve
|
||||
your standard, plain-text `.ged` file directly.
|
||||
- **Zipped Archive (`.gdz` or `.zip`)**: If you want to bundle family photos,
|
||||
you compress the `.ged` file and the photos together into a single archive.
|
||||
Topola Viewer will automatically unzip and map the photos to the tree.
|
||||
Serving this as a single file (either `.ged` or `.gdz`) keeps the container
|
||||
setup simple and highly portable.
|
||||
|
||||
## 3. Alternative Designs Considered & Rejected
|
||||
|
||||
To ensure future development does not regress or re-argue established choices, this section documents the architectural patterns that were thoroughly evaluated but ultimately rejected.
|
||||
To ensure future development does not regress or re-argue established choices,
|
||||
this section documents the architectural patterns that were thoroughly evaluated
|
||||
but ultimately rejected.
|
||||
|
||||
### Option A: Build-Time Variable Injection (`VITE_STATIC_URL`)
|
||||
|
||||
* **Design Proposal**: Build custom container images by setting `VITE_STATIC_URL` during the Vite production build step (`npm run build`) inside the Dockerfile.
|
||||
* **Why Rejected**:
|
||||
* **Heavy & Slow Builds**: Recompiling a modern React SPA inside Docker requires a full Node.js runtime, downloading `node_modules`, and compiling TypeScript. This takes minutes, consumes significant system resources, and makes building a custom image locally a highly friction-filled developer experience.
|
||||
* **No Runtime Dynamism**: Since the static URL is hardcoded into minified JS assets during compiling, users cannot change their genealogy file path dynamically when running the container. A simple task like pointing the container to a new GEDCOM file would require a full image rebuild, rather than a simple environment variable or volume mount adjustment.
|
||||
- **Design Proposal**: Build custom container images by setting
|
||||
`VITE_STATIC_URL` during the Vite production build step (`npm run build`)
|
||||
inside the Dockerfile.
|
||||
- **Why Rejected**:
|
||||
- **Heavy & Slow Builds**: Recompiling a modern React SPA inside Docker
|
||||
requires a full Node.js runtime, downloading `node_modules`, and compiling
|
||||
TypeScript. This takes minutes, consumes significant system resources, and
|
||||
makes building a custom image locally a highly friction-filled developer
|
||||
experience.
|
||||
- **No Runtime Dynamism**: Since the static URL is hardcoded into minified JS
|
||||
assets during compiling, users cannot change their genealogy file path
|
||||
dynamically when running the container. A simple task like pointing the
|
||||
container to a new GEDCOM file would require a full image rebuild, rather
|
||||
than a simple environment variable or volume mount adjustment.
|
||||
|
||||
### Option B: Client-Side Config Fetching (`fetch('/config.json')`)
|
||||
|
||||
* **Design Proposal**: At React application startup, trigger an asynchronous HTTP request (`fetch('/config.json')`) to retrieve the configuration and target tree location.
|
||||
* **Why Rejected**:
|
||||
* **Unnecessary Startup Latency**: Every asynchronous network call in a client-side SPA blocks the React application mount lifecycle. Even on high-performance servers, fetching `/config.json` introduces an extra network roundtrip at boot time.
|
||||
- **Design Proposal**: At React application startup, trigger an asynchronous
|
||||
HTTP request (`fetch('/config.json')`) to retrieve the configuration and
|
||||
target tree location.
|
||||
- **Why Rejected**:
|
||||
- **Unnecessary Startup Latency**: Every asynchronous network call in a
|
||||
client-side SPA blocks the React application mount lifecycle. Even on
|
||||
high-performance servers, fetching `/config.json` introduces an extra
|
||||
network roundtrip at boot time.
|
||||
|
||||
### Option C: Shell-Based Template Rendering (Nginx Alpine + `envsubst`)
|
||||
|
||||
* **Design Proposal**: Run the static application on an Nginx Alpine base container, using a startup shell script and the `envsubst` tool to substitute environment variables inside `index.html` before launching the web server.
|
||||
* **Why Rejected**:
|
||||
* **Vulnerability Attack Surface**: Standard Linux and Alpine base images contain shell environments (`/bin/sh`), package managers (`apk`), and standard operating system utilities. These represent a non-zero container attack surface, leading to vulnerability alerts (CVEs) in enterprise environments.
|
||||
- **Design Proposal**: Run the static application on an Nginx Alpine base
|
||||
container, using a startup shell script and the `envsubst` tool to substitute
|
||||
environment variables inside `index.html` before launching the web server.
|
||||
- **Why Rejected**:
|
||||
- **Vulnerability Attack Surface**: Standard Linux and Alpine base images
|
||||
contain shell environments (`/bin/sh`), package managers (`apk`), and
|
||||
standard operating system utilities. These represent a non-zero container
|
||||
attack surface, leading to vulnerability alerts (CVEs) in enterprise
|
||||
environments.
|
||||
|
||||
### Option D: Custom Compiled Go Static Server
|
||||
|
||||
* **Design Proposal**: Compile a custom, lightweight 35-line Go static web server program that handles SPA routing and dynamically injects environment variables directly into the `index.html` served from memory.
|
||||
* **Why Rejected**:
|
||||
* **Maintenance Complexity Overhead**: Although highly performant and lightweight (~10MB total container size), introducing custom compiled server source code adds to repository maintenance. Developers would have to test, audit, and maintain custom Go HTTP routing logic alongside their main React/TypeScript codebase. Using an off-the-shelf server (Caddy) eliminates this maintenance entirely.
|
||||
- **Design Proposal**: Compile a custom, lightweight 35-line Go static web
|
||||
server program that handles SPA routing and dynamically injects environment
|
||||
variables directly into the `index.html` served from memory.
|
||||
- **Why Rejected**:
|
||||
- **Maintenance Complexity Overhead**: Although highly performant and
|
||||
lightweight (~10MB total container size), introducing custom compiled server
|
||||
source code adds to repository maintenance. Developers would have to test,
|
||||
audit, and maintain custom Go HTTP routing logic alongside their main
|
||||
React/TypeScript codebase. Using an off-the-shelf server (Caddy) eliminates
|
||||
this maintenance entirely.
|
||||
|
||||
### Option E: Multi-Architecture Image Support (`linux/arm64`)
|
||||
|
||||
* **Design Proposal**: Package and publish multi-architecture container images targeting both standard x86_64 (`linux/amd64`) and ARM64 (`linux/arm64`) platforms.
|
||||
* **Why Rejected**:
|
||||
* **Pipeline Emulation Latency**: Building multi-architecture images on standard AMD64 GitHub Actions runners requires virtualized instruction emulation via QEMU. Emulating the TypeScript build and Go compiler inside a QEMU virtual environment increases container compilation cycles by up to 10x to 20x, significantly bloating release delays.
|
||||
* **No Server-Side Execution Penalty**: Topola Viewer is a pure, client-side React single-page application (SPA). The browser executes all chart rendering and data logic on the end-user's computer (whether it runs on x86_64, ARM64/Apple Silicon, or mobile platforms). The container's internal web server (Caddy) simply serves static HTML/JS assets. Running the `linux/amd64` image under standard Docker architecture translation (e.g., Rosetta 2 or Docker Desktop VM) on ARM64/M-series hosts has absolutely zero visible performance penalty.
|
||||
* **Workflow Stability**: Focusing exclusively on `linux/amd64` standardizes our GitHub Actions runner steps, completely removes complex third-party dependencies like `setup-qemu-action`, and ensures build cycles remain blazingly fast, secure, and reliable.
|
||||
- **Design Proposal**: Package and publish multi-architecture container images
|
||||
targeting both standard x86_64 (`linux/amd64`) and ARM64 (`linux/arm64`)
|
||||
platforms.
|
||||
- **Why Rejected**:
|
||||
- **Pipeline Emulation Latency**: Building multi-architecture images on
|
||||
standard AMD64 GitHub Actions runners requires virtualized instruction
|
||||
emulation via QEMU. Emulating the TypeScript build and Go compiler inside a
|
||||
QEMU virtual environment increases container compilation cycles by up to 10x
|
||||
to 20x, significantly bloating release delays.
|
||||
- **No Server-Side Execution Penalty**: Topola Viewer is a pure, client-side
|
||||
React single-page application (SPA). The browser executes all chart
|
||||
rendering and data logic on the end-user's computer (whether it runs on
|
||||
x86_64, ARM64/Apple Silicon, or mobile platforms). The container's internal
|
||||
web server (Caddy) simply serves static HTML/JS assets. Running the
|
||||
`linux/amd64` image under standard Docker architecture translation (e.g.,
|
||||
Rosetta 2 or Docker Desktop VM) on ARM64/M-series hosts has absolutely zero
|
||||
visible performance penalty.
|
||||
- **Workflow Stability**: Focusing exclusively on `linux/amd64` standardizes
|
||||
our GitHub Actions runner steps, completely removes complex third-party
|
||||
dependencies like `setup-qemu-action`, and ensures build cycles remain
|
||||
blazingly fast, secure, and reliable.
|
||||
|
||||
## 4. Detailed Implementation Plan
|
||||
|
||||
This section outlines the complete, step-by-step implementation plan. It lists every file that will be modified or created, the exact code modifications or configurations required, and the explicit engineering rationale for each.
|
||||
This section outlines the complete, step-by-step implementation plan. It lists
|
||||
every file that will be modified or created, the exact code modifications or
|
||||
configurations required, and the explicit engineering rationale for each.
|
||||
|
||||
### Step 1: Add Global Config Injection Target to HTML
|
||||
|
||||
* **Target File**: [index.html](../index.html) (Modify)
|
||||
* **Action**: Add a `<meta>` tag containing the dynamic injection placeholder inside the `<head>` tag:
|
||||
- **Target File**: [index.html](../index.html) (Modify)
|
||||
- **Action**: Add a `<meta>` tag containing the dynamic injection placeholder
|
||||
inside the `<head>` tag:
|
||||
```html
|
||||
<meta name="topola-static-url" content="{{ env `STATIC_URL` | html }}">
|
||||
<meta name="topola-static-url" content="{{ env `STATIC_URL` | html }}" />
|
||||
```
|
||||
* **Rationale**:
|
||||
* Storing the configuration safely inside a `<meta>` tag content attribute ensures it is parsed strictly as data rather than executable code, preventing Reflected Cross-Site Scripting (XSS) or script injection vulnerabilities.
|
||||
* **HTML Parsing Protection**: Enclosing the template expression in backticks (`` `STATIC_URL` ``) prevents syntactically broken nested double quotes in the HTML `content` attribute, which would otherwise break standard browser HTML tag parsing.
|
||||
* The Caddy `| html` filter guarantees that the environment variable is fully HTML-entity-escaped before injection.
|
||||
* The placeholder syntax `{{ env `STATIC_URL` | html }}` is evaluated dynamically by Caddy when serving the page, adding zero network latency.
|
||||
- **Rationale**:
|
||||
- Storing the configuration safely inside a `<meta>` tag content attribute
|
||||
ensures it is parsed strictly as data rather than executable code,
|
||||
preventing Reflected Cross-Site Scripting (XSS) or script injection
|
||||
vulnerabilities.
|
||||
- **HTML Parsing Protection**: Enclosing the template expression in backticks
|
||||
(`` `STATIC_URL` ``) prevents syntactically broken nested double quotes in
|
||||
the HTML `content` attribute, which would otherwise break standard browser
|
||||
HTML tag parsing.
|
||||
- The Caddy `| html` filter guarantees that the environment variable is fully
|
||||
HTML-entity-escaped before injection.
|
||||
- The placeholder syntax `{{ env `STATIC_URL` | html }}` is evaluated
|
||||
dynamically by Caddy when serving the page, adding zero network latency.
|
||||
|
||||
### Step 2: Update Application Boot Logic to Handle Global Config
|
||||
* **Target File**: [src/app.tsx](../src/app.tsx) (Modify)
|
||||
* **Action**: Update the application boot logic to handle dynamic config via the `<meta>` tag or Vite static URL, override standalone/CORS properties, and adjust route settings:
|
||||
* **Config Resolution**: Retrieve the statically-served GEDCOM/GDZ URL.
|
||||
* **Arguments Setup**: Disable standalone mode and bypass CORS handling when a static URL is provided.
|
||||
* **Conditional Routing**: Force routing directly to `/view` (bypassing the standard intro landing page) when `staticUrl` is set.
|
||||
|
||||
- **Target File**: [src/app.tsx](../src/app.tsx) (Modify)
|
||||
- **Action**: Update the application boot logic to handle dynamic config via the
|
||||
`<meta>` tag or Vite static URL, override standalone/CORS properties, and
|
||||
adjust route settings:
|
||||
- **Config Resolution**: Retrieve the statically-served GEDCOM/GDZ URL.
|
||||
- **Arguments Setup**: Disable standalone mode and bypass CORS handling when a
|
||||
static URL is provided.
|
||||
- **Conditional Routing**: Force routing directly to `/view` (bypassing the
|
||||
standard intro landing page) when `staticUrl` is set.
|
||||
|
||||
```typescript
|
||||
// 1. Global Config Resolution
|
||||
function getStaticUrl(): string | undefined {
|
||||
@@ -122,14 +207,26 @@ This section outlines the complete, step-by-step implementation plan. It lists e
|
||||
</Routes>
|
||||
)}
|
||||
```
|
||||
* **Rationale**:
|
||||
* **Security & Safety**: Retrieving configuration via the DOM's `<meta>` element ensures we parse data context safely rather than relying on direct executable script template injection.
|
||||
* **UX Modularity (Zero-Friction Mode)**: Bypassing the Intro page and disabling the standard "open file" menus (`standalone: false`) transforms the general-purpose viewer into a streamlined, dedicated instance for the preloaded tree.
|
||||
* **Backward Compatibility**: Keeps standard dev runs (`npm start`) and static deployments (GitHub Pages/WikiTree) working out-of-the-box because the template string `{{ env "STATIC_URL" }}` is safely ignored when it hasn't been evaluated by Caddy.
|
||||
|
||||
- **Rationale**:
|
||||
- **Security & Safety**: Retrieving configuration via the DOM's `<meta>`
|
||||
element ensures we parse data context safely rather than relying on direct
|
||||
executable script template injection.
|
||||
- **UX Modularity (Zero-Friction Mode)**: Bypassing the Intro page and
|
||||
disabling the standard "open file" menus (`standalone: false`) transforms
|
||||
the general-purpose viewer into a streamlined, dedicated instance for the
|
||||
preloaded tree.
|
||||
- **Backward Compatibility**: Keeps standard dev runs (`npm start`) and static
|
||||
deployments (GitHub Pages/WikiTree) working out-of-the-box because the
|
||||
template string `{{ env "STATIC_URL" }}` is safely ignored when it hasn't
|
||||
been evaluated by Caddy.
|
||||
|
||||
### Step 3: Create Caddy Server Configuration
|
||||
* **Target File**: `docker/Caddyfile` (New File in dedicated directory)
|
||||
* **Action**: Add Caddy serving rules to handle robust security headers, optimized caching, SPA routing, and active template evaluation:
|
||||
|
||||
- **Target File**: `docker/Caddyfile` (New File in dedicated directory)
|
||||
- **Action**: Add Caddy serving rules to handle robust security headers,
|
||||
optimized caching, SPA routing, and active template evaluation:
|
||||
|
||||
```caddy
|
||||
{
|
||||
# Disable administrative API to prevent permission errors in read-only environments
|
||||
@@ -138,10 +235,10 @@ This section outlines the complete, step-by-step implementation plan. It lists e
|
||||
|
||||
:8080 {
|
||||
root * /app/public
|
||||
|
||||
|
||||
# Compress static assets (HTML, JS, CSS, GEDCOM text files)
|
||||
encode gzip zstd
|
||||
|
||||
|
||||
file_server
|
||||
|
||||
# Robust Security Headers
|
||||
@@ -173,19 +270,36 @@ This section outlines the complete, step-by-step implementation plan. It lists e
|
||||
try_files {path} /index.html
|
||||
}
|
||||
```
|
||||
* **Rationale**:
|
||||
* **Security Headers**: Protects production deployments against clickjacking, MIME-sniffing, and referrer leaks through standard secure response headers.
|
||||
* **Disable Admin Endpoint**: Setting `admin off` prevents Caddy from attempting to bind administrative sockets or write auto-saved configuration files (`caddy_autosave.json`) to its working directory, avoiding fatal startup failures in rootless containers and secure read-only filesystems.
|
||||
* **Asset Compression**: Adding `encode gzip zstd` ensures that large client-side JS bundles and plain-text GEDCOM files are compressed, dramatically reducing load-time latency and server bandwidth costs.
|
||||
* **Precise Caching Rules**: Restructures caching to only apply `immutable` tags to assets in `/assets/*` (which contains Vite's hashed JS/CSS files). This ensures user-supplied dynamic family trees (`.ged`, `.gdz`) and mounted photos do not get cached permanently, allowing instant runtime updates.
|
||||
* **HTML Templating**: Caddy dynamically processes variables (like `{{ env `STATIC_URL` | html }}`) for HTML documents, avoiding performance overhead on other resources.
|
||||
* **Non-Root Port**: Serving from port `8080` permits the container to run as an unprivileged user without root capabilities.
|
||||
|
||||
- **Rationale**:
|
||||
- **Security Headers**: Protects production deployments against clickjacking,
|
||||
MIME-sniffing, and referrer leaks through standard secure response headers.
|
||||
- **Disable Admin Endpoint**: Setting `admin off` prevents Caddy from
|
||||
attempting to bind administrative sockets or write auto-saved configuration
|
||||
files (`caddy_autosave.json`) to its working directory, avoiding fatal
|
||||
startup failures in rootless containers and secure read-only filesystems.
|
||||
- **Asset Compression**: Adding `encode gzip zstd` ensures that large
|
||||
client-side JS bundles and plain-text GEDCOM files are compressed,
|
||||
dramatically reducing load-time latency and server bandwidth costs.
|
||||
- **Precise Caching Rules**: Restructures caching to only apply `immutable`
|
||||
tags to assets in `/assets/*` (which contains Vite's hashed JS/CSS files).
|
||||
This ensures user-supplied dynamic family trees (`.ged`, `.gdz`) and mounted
|
||||
photos do not get cached permanently, allowing instant runtime updates.
|
||||
- **HTML Templating**: Caddy dynamically processes variables (like
|
||||
`{{ env `STATIC_URL` | html }}`) for HTML documents, avoiding performance
|
||||
overhead on other resources.
|
||||
- **Non-Root Port**: Serving from port `8080` permits the container to run as
|
||||
an unprivileged user without root capabilities.
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Create Multi-Stage Dockerfile
|
||||
* **Target File**: `docker/Dockerfile` (New File in dedicated directory)
|
||||
* **Action**: Write the multi-stage compilation and assembly pipeline using a pre-compiled Caddy binary and Google's library-free Distroless Static base image running as nonroot:
|
||||
|
||||
- **Target File**: `docker/Dockerfile` (New File in dedicated directory)
|
||||
- **Action**: Write the multi-stage compilation and assembly pipeline using a
|
||||
pre-compiled Caddy binary and Google's library-free Distroless Static base
|
||||
image running as nonroot:
|
||||
|
||||
```dockerfile
|
||||
# Stage 1: Compile the React/TypeScript bundle
|
||||
FROM node:20-alpine AS react-builder
|
||||
@@ -212,16 +326,34 @@ This section outlines the complete, step-by-step implementation plan. It lists e
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/usr/bin/caddy", "run", "--config", "./Caddyfile", "--adapter", "caddyfile"]
|
||||
```
|
||||
* **Rationale**:
|
||||
* **Clean Compilation Separation**: Restructures compilation using a multi-stage build where the heavy Node.js and TypeScript compiler packages are restricted entirely to the builder stage, keeping the final runner image extremely lightweight and free of development tools.
|
||||
* **Static Caddy Binary**: Copying the pre-compiled, statically linked Caddy binary directly from the official `caddy` Alpine image. Since the official Caddy binary is a pure, library-independent Go executable built without CGO, it runs flawlessly on top of a Google Distroless Static image, completely bypassing the need to compile Caddy from source and reducing build times by several minutes.
|
||||
* **Distroless Static Base**: Standardizing on `distroless/static-debian12` removes *all* dynamic libraries, shells, and packages from the runtime container. This cuts container image size to under ~25MB and eliminates runtime vulnerability scan alerts (CVEs) completely.
|
||||
* **Non-Root Execution**: Switching to `USER nonroot:nonroot` inside the production stage ensures the application runs with minimum privileges, satisfying strict enterprise and Kubernetes execution policies.
|
||||
* **Explicit File Ownership**: Using `COPY --chown=nonroot:nonroot` guarantees that all files in the runtime container are owned by the runtime non-root user, bypassing permissions conflicts.
|
||||
|
||||
- **Rationale**:
|
||||
- **Clean Compilation Separation**: Restructures compilation using a
|
||||
multi-stage build where the heavy Node.js and TypeScript compiler packages
|
||||
are restricted entirely to the builder stage, keeping the final runner image
|
||||
extremely lightweight and free of development tools.
|
||||
- **Static Caddy Binary**: Copying the pre-compiled, statically linked Caddy
|
||||
binary directly from the official `caddy` Alpine image. Since the official
|
||||
Caddy binary is a pure, library-independent Go executable built without CGO,
|
||||
it runs flawlessly on top of a Google Distroless Static image, completely
|
||||
bypassing the need to compile Caddy from source and reducing build times by
|
||||
several minutes.
|
||||
- **Distroless Static Base**: Standardizing on `distroless/static-debian12`
|
||||
removes _all_ dynamic libraries, shells, and packages from the runtime
|
||||
container. This cuts container image size to under ~25MB and eliminates
|
||||
runtime vulnerability scan alerts (CVEs) completely.
|
||||
- **Non-Root Execution**: Switching to `USER nonroot:nonroot` inside the
|
||||
production stage ensures the application runs with minimum privileges,
|
||||
satisfying strict enterprise and Kubernetes execution policies.
|
||||
- **Explicit File Ownership**: Using `COPY --chown=nonroot:nonroot` guarantees
|
||||
that all files in the runtime container are owned by the runtime non-root
|
||||
user, bypassing permissions conflicts.
|
||||
|
||||
### Step 5: Create Docker Ignore File
|
||||
* **Target File**: `.dockerignore` (New File at root)
|
||||
* **Action**: Exclude local development folders and build environments from entering the Docker context:
|
||||
|
||||
- **Target File**: `.dockerignore` (New File at root)
|
||||
- **Action**: Exclude local development folders and build environments from
|
||||
entering the Docker context:
|
||||
```text
|
||||
node_modules
|
||||
dist
|
||||
@@ -231,12 +363,17 @@ This section outlines the complete, step-by-step implementation plan. It lists e
|
||||
.vscode
|
||||
README.md
|
||||
```
|
||||
* **Rationale**:
|
||||
* Speeds up local `docker build` times by preventing megabytes of local folders (like `node_modules` and local `dist` directories) from uploading to the Docker daemon build context.
|
||||
- **Rationale**:
|
||||
- Speeds up local `docker build` times by preventing megabytes of local
|
||||
folders (like `node_modules` and local `dist` directories) from uploading to
|
||||
the Docker daemon build context.
|
||||
|
||||
### Step 6: Create GitHub Actions Container Deployment Workflow
|
||||
* **Target File**: `.github/workflows/deploy-docker.yml` (New File)
|
||||
* **Action**: Setup a GitHub Actions workflow to compile, tag, and publish the container images to GHCR, handling lowercase names and safe tagging:
|
||||
|
||||
- **Target File**: `.github/workflows/deploy-docker.yml` (New File)
|
||||
- **Action**: Setup a GitHub Actions workflow to compile, tag, and publish the
|
||||
container images to GHCR, handling lowercase names and safe tagging:
|
||||
|
||||
```yaml
|
||||
name: Build and Publish Docker Image
|
||||
|
||||
@@ -252,55 +389,71 @@ This section outlines the complete, step-by-step implementation plan. It lists e
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Convert Repository Name to Lowercase
|
||||
run: |
|
||||
echo "GHCR_IMAGE_NAME=$(echo "ghcr.io/${{ github.repository }}" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||
- name: Convert Repository Name to Lowercase
|
||||
run: |
|
||||
echo "GHCR_IMAGE_NAME=$(echo "ghcr.io/${{ github.repository }}" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.GHCR_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.GHCR_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
```
|
||||
* **Rationale**:
|
||||
* **Strict Lowercase Registry Names**: Registry image names must be strictly lowercase. Converting the repository path to lowercase prevents Docker push failures due to uppercase organization or repository names (e.g. `PeWu`).
|
||||
* **Safe Production Tagging**: Restricts pushing the `latest` tag to master branch runs only, preventing development branches or forks from accidentally overwriting the stable master production image.
|
||||
* **No QEMU Dependency**: Because the image targets standard `linux/amd64` servers directly, we bypass slow CPU instruction emulation completely. This eliminates the `setup-qemu-action` dependency, protecting the workflow against virtualizer crashes and speeding up build initialization.
|
||||
* **Git SHA Tagging**: Generates git SHA tags automatically, allowing precise auditing and rolling back of deployments.
|
||||
* **Modern Actions Standard**: Upgrades all critical deployment actions to their latest major releases to optimize runner speeds, secure security improvements, and align with Node 20/22 GitHub Action standard runner specifications.
|
||||
* **Active GitHub Caching**: Leverages standard action build cache stores to reuse layers and accelerate build cycles.
|
||||
|
||||
- **Rationale**:
|
||||
- **Strict Lowercase Registry Names**: Registry image names must be strictly
|
||||
lowercase. Converting the repository path to lowercase prevents Docker push
|
||||
failures due to uppercase organization or repository names (e.g. `PeWu`).
|
||||
- **Safe Production Tagging**: Restricts pushing the `latest` tag to master
|
||||
branch runs only, preventing development branches or forks from accidentally
|
||||
overwriting the stable master production image.
|
||||
- **No QEMU Dependency**: Because the image targets standard `linux/amd64`
|
||||
servers directly, we bypass slow CPU instruction emulation completely. This
|
||||
eliminates the `setup-qemu-action` dependency, protecting the workflow
|
||||
against virtualizer crashes and speeding up build initialization.
|
||||
- **Git SHA Tagging**: Generates git SHA tags automatically, allowing precise
|
||||
auditing and rolling back of deployments.
|
||||
- **Modern Actions Standard**: Upgrades all critical deployment actions to
|
||||
their latest major releases to optimize runner speeds, secure security
|
||||
improvements, and align with Node 20/22 GitHub Action standard runner
|
||||
specifications.
|
||||
- **Active GitHub Caching**: Leverages standard action build cache stores to
|
||||
reuse layers and accelerate build cycles.
|
||||
|
||||
### Step 7: Couple Docker Publication to existing Main Deployment Pipeline
|
||||
* **Target File**: [.github/workflows/deploy-everywhere.yml](deploy-everywhere.yml) (Modify)
|
||||
* **Action**: Integrate the newly created Docker workflow as a concurrent job:
|
||||
|
||||
- **Target File**:
|
||||
[.github/workflows/deploy-everywhere.yml](deploy-everywhere.yml) (Modify)
|
||||
- **Action**: Integrate the newly created Docker workflow as a concurrent job:
|
||||
|
||||
```yaml
|
||||
name: Deploy everywhere
|
||||
|
||||
@@ -318,130 +471,201 @@ This section outlines the complete, step-by-step implementation plan. It lists e
|
||||
uses: ./.github/workflows/deploy-docker.yml
|
||||
secrets: inherit
|
||||
```
|
||||
* **Rationale**:
|
||||
* Integrates the container deployment pipeline seamlessly into the main release trigger (`deploy-everywhere`), ensuring that Docker, GH Pages, and WikiTree versions are always updated in lockstep.
|
||||
|
||||
- **Rationale**:
|
||||
- Integrates the container deployment pipeline seamlessly into the main
|
||||
release trigger (`deploy-everywhere`), ensuring that Docker, GH Pages, and
|
||||
WikiTree versions are always updated in lockstep.
|
||||
|
||||
### Step 8: Document Container Usage in Main README
|
||||
* **Target File**: `README.md` (Modify)
|
||||
* **Action**: Add a dedicated "Docker Container Deployment" section with clear run, build, mount, and standalone template instructions:
|
||||
```markdown
|
||||
|
||||
- **Target File**: `README.md` (Modify)
|
||||
- **Action**: Add a dedicated "Docker Container Deployment" section with clear
|
||||
run, build, mount, and standalone template instructions:
|
||||
|
||||
````markdown
|
||||
## Docker Container Deployment
|
||||
|
||||
Topola Viewer can be run locally or deployed to standard cloud environments using Docker.
|
||||
Topola Viewer can be run locally or deployed to standard cloud environments
|
||||
using Docker.
|
||||
|
||||
### Running Topola Viewer
|
||||
|
||||
To pull and run Topola Viewer:
|
||||
|
||||
```bash
|
||||
docker run -d -p 8080:8080 ghcr.io/pewu/topola-viewer:latest
|
||||
```
|
||||
Open your web browser and go to `http://localhost:8080` to upload your family tree files locally.
|
||||
````
|
||||
|
||||
Open your web browser and go to `http://localhost:8080` to upload your family
|
||||
tree files locally.
|
||||
|
||||
### Running with Your Own Data (Zero-Build Run)
|
||||
You can serve a standalone, pre-loaded family tree with zero compilation by mounting your family tree data (a `.ged` file or a zipped `.gdz` archive containing photos) directly into the running container:
|
||||
|
||||
You can serve a standalone, pre-loaded family tree with zero compilation by
|
||||
mounting your family tree data (a `.ged` file or a zipped `.gdz` archive
|
||||
containing photos) directly into the running container:
|
||||
|
||||
```bash
|
||||
docker run -d -p 8080:8080 \
|
||||
-e STATIC_URL=my_family.gdz \
|
||||
-v ./my_family.gdz:/app/public/my_family.gdz \
|
||||
ghcr.io/pewu/topola-viewer:latest
|
||||
```
|
||||
|
||||
|
||||
### Building the Base Image Locally
|
||||
|
||||
To build the base image from source:
|
||||
|
||||
```bash
|
||||
docker build -t topola-viewer -f docker/Dockerfile .
|
||||
```
|
||||
|
||||
### Ready-To-Use Standalone Templates
|
||||
For creating completely self-contained Docker images that bundle your genealogy data and serve it instantly, see these pre-configured examples:
|
||||
|
||||
1. **[Simple Standalone Tree](docker/examples/simple/)**: Demonstrates how to package and pre-load a `.ged` file directly inside a custom image.
|
||||
2. **[Standalone Tree with Photos](docker/examples/photos/)**: Packages your family tree and a `photos/` folder into a valid `.gdz` archive on-the-fly.
|
||||
For creating completely self-contained Docker images that bundle your
|
||||
genealogy data and serve it instantly, see these pre-configured examples:
|
||||
1. **[Simple Standalone Tree](docker/examples/simple/)**: Demonstrates how to
|
||||
package and pre-load a `.ged` file directly inside a custom image.
|
||||
2. **[Standalone Tree with Photos](docker/examples/photos/)**: Packages your
|
||||
family tree and a `photos/` folder into a valid `.gdz` archive on-the-fly.
|
||||
|
||||
```
|
||||
* **Rationale**:
|
||||
* Ensures the new Docker feature has first-class visibility, clear instructions, and easy references to packaged standalone templates for end users, enabling both basic runs, volume-mounted local data serving, and built-in custom imagery.
|
||||
|
||||
```
|
||||
|
||||
- **Rationale**:
|
||||
- Ensures the new Docker feature has first-class visibility, clear
|
||||
instructions, and easy references to packaged standalone templates for end
|
||||
users, enabling both basic runs, volume-mounted local data serving, and
|
||||
built-in custom imagery.
|
||||
|
||||
### Step 9: Provide Custom Image Templates (Simple & Zipped on the Fly)
|
||||
* **Target Files**:
|
||||
* `docker/examples/simple/Dockerfile` (New File)
|
||||
* `docker/examples/simple/README.md` (New File)
|
||||
* `docker/examples/simple/family.ged` (New simple, valid example GEDCOM file)
|
||||
* `docker/examples/photos/Dockerfile` (New File)
|
||||
* `docker/examples/photos/README.md` (New File)
|
||||
* `docker/examples/photos/family.ged` (New simple, valid example GEDCOM file)
|
||||
* `docker/examples/photos/photos/I1.jpg` (New simple, valid example photo asset)
|
||||
* `docker/examples/photos/photos/I2.jpg` (New simple, valid example photo asset)
|
||||
* **Action**: Create two dedicated subdirectories containing turnkey templates. To make these examples instantly runnable out-of-the-box, we provide simple, valid example files (`family.ged`, `I1.jpg`, and `I2.jpg`) inside the repository so users can immediately run `docker build` and test the container features without having to prepare their own private files first.
|
||||
|
||||
- **Target Files**:
|
||||
- `docker/examples/simple/Dockerfile` (New File)
|
||||
- `docker/examples/simple/README.md` (New File)
|
||||
- `docker/examples/simple/family.ged` (New simple, valid example GEDCOM file)
|
||||
- `docker/examples/photos/Dockerfile` (New File)
|
||||
- `docker/examples/photos/README.md` (New File)
|
||||
- `docker/examples/photos/family.ged` (New simple, valid example GEDCOM file)
|
||||
- `docker/examples/photos/photos/I1.jpg` (New simple, valid example photo
|
||||
asset)
|
||||
- `docker/examples/photos/photos/I2.jpg` (New simple, valid example photo
|
||||
asset)
|
||||
- **Action**: Create two dedicated subdirectories containing turnkey templates.
|
||||
To make these examples instantly runnable out-of-the-box, we provide simple,
|
||||
valid example files (`family.ged`, `I1.jpg`, and `I2.jpg`) inside the
|
||||
repository so users can immediately run `docker build` and test the container
|
||||
features without having to prepare their own private files first.
|
||||
|
||||
#### 1. Simple GEDCOM Template (`docker/examples/simple`)
|
||||
* **`docker/examples/simple/Dockerfile`**:
|
||||
```dockerfile
|
||||
# Start from the official compiled container
|
||||
FROM ghcr.io/pewu/topola-viewer:latest
|
||||
|
||||
# Copy the unzipped GEDCOM file directly into public folder
|
||||
COPY family.ged /app/public/family.ged
|
||||
- **`docker/examples/simple/Dockerfile`**:
|
||||
|
||||
# Configure server to pre-load this raw GEDCOM file
|
||||
ENV STATIC_URL=family.ged
|
||||
```
|
||||
* **`docker/examples/simple/README.md`**:
|
||||
```markdown
|
||||
# Standalone GEDCOM Container Example
|
||||
```dockerfile
|
||||
# Start from the official compiled container
|
||||
FROM ghcr.io/pewu/topola-viewer:latest
|
||||
|
||||
This example builds a self-contained image that hosts a single `.ged` file directly (no photos).
|
||||
# Copy the unzipped GEDCOM file directly into public folder
|
||||
COPY family.ged /app/public/family.ged
|
||||
|
||||
## Instructions
|
||||
# Configure server to pre-load this raw GEDCOM file
|
||||
ENV STATIC_URL=family.ged
|
||||
```
|
||||
|
||||
1. Put your GEDCOM file in this directory and name it `family.ged`.
|
||||
2. Build your custom container:
|
||||
```bash
|
||||
docker build -t my-simple-tree .
|
||||
```
|
||||
3. Run your container:
|
||||
```bash
|
||||
docker run -d -p 8080:8080 my-simple-tree
|
||||
```
|
||||
```
|
||||
- **`docker/examples/simple/README.md`**:
|
||||
|
||||
````markdown
|
||||
# Standalone GEDCOM Container Example
|
||||
|
||||
This example builds a self-contained image that hosts a single `.ged` file
|
||||
directly (no photos).
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Put your GEDCOM file in this directory and name it `family.ged`.
|
||||
2. Build your custom container:
|
||||
```bash
|
||||
docker build -t my-simple-tree .
|
||||
```
|
||||
````
|
||||
|
||||
3. Run your container:
|
||||
```bash
|
||||
docker run -d -p 8080:8080 my-simple-tree
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
#### 2. Zipped Media Template (`docker/examples/photos`)
|
||||
* **`docker/examples/photos/Dockerfile`**:
|
||||
```dockerfile
|
||||
# Stage 1: Multi-stage helper to zip GEDCOM & photos together preserving directory structure
|
||||
FROM alpine:latest AS zipper
|
||||
RUN apk add --no-cache zip
|
||||
WORKDIR /build
|
||||
COPY family.ged ./
|
||||
COPY photos/ ./photos/
|
||||
# Zip contents relative to build root to preserve directories as referenced in GEDCOM
|
||||
RUN zip -r family.gdz family.ged photos/
|
||||
|
||||
# Stage 2: Load the zip file into the official container
|
||||
FROM ghcr.io/pewu/topola-viewer:latest
|
||||
COPY --from=zipper /build/family.gdz /app/public/family.gdz
|
||||
ENV STATIC_URL=family.gdz
|
||||
```
|
||||
* **`docker/examples/photos/README.md`**:
|
||||
```markdown
|
||||
# Standalone Zipped Family Tree Container with Photos
|
||||
- **`docker/examples/photos/Dockerfile`**:
|
||||
|
||||
This example leverages a multi-stage Docker build to automatically compress your `.ged` file and `photos/` folder into a secure `.gdz` archive on-the-fly, preserving your image directory path structures.
|
||||
```dockerfile
|
||||
# Stage 1: Multi-stage helper to zip GEDCOM & photos together preserving directory structure
|
||||
FROM alpine:latest AS zipper
|
||||
RUN apk add --no-cache zip
|
||||
WORKDIR /build
|
||||
COPY family.ged ./
|
||||
COPY photos/ ./photos/
|
||||
# Zip contents relative to build root to preserve directories as referenced in GEDCOM
|
||||
RUN zip -r family.gdz family.ged photos/
|
||||
|
||||
## Structure
|
||||
* Place your `family.ged` file here.
|
||||
* Place your photos also in this directory, or inside a `photos/` folder in this directory. If you put the photos in the `photos/` directory, make sure your GEDCOM file contains file references containing the `photos/` prefix. See the sample [family.ged](family.ged).
|
||||
# Stage 2: Load the zip file into the official container
|
||||
FROM ghcr.io/pewu/topola-viewer:latest
|
||||
COPY --from=zipper /build/family.gdz /app/public/family.gdz
|
||||
ENV STATIC_URL=family.gdz
|
||||
```
|
||||
|
||||
## Instructions
|
||||
- **`docker/examples/photos/README.md`**:
|
||||
|
||||
1. Build your custom container:
|
||||
```bash
|
||||
docker build -t my-photo-tree .
|
||||
```
|
||||
2. Run your container:
|
||||
```bash
|
||||
docker run -d -p 8080:8080 my-photo-tree
|
||||
```
|
||||
```
|
||||
* **Rationale**:
|
||||
* **Simple Template**: Demonstrates the standard, zero-friction path for users who just have a raw `.ged` file.
|
||||
* **Photos Template (On-The-Fly Zipper)**: Solves the problem of executing commands in a shell-less, commandless distroless container. By spinning up a lightweight Alpine zipper image to execute the native `zip` tool, and copying *only* the finished `.gdz` artifact into the final stage, we achieve a completely self-contained, secure target image.
|
||||
* **Preserved Path Zip File Structure**: Packaging photos by zipping from the build root (`zip -r family.gdz family.ged photos/`) preserves the exact folder hierarchy relative to the GEDCOM. This guarantees that any complex or structured media folders (e.g., `photos/1990s/wedding.jpg`) match the exact file references declared inside the GEDCOM file, avoiding broken images from flattened zip scopes.
|
||||
````markdown
|
||||
# Standalone Zipped Family Tree Container with Photos
|
||||
|
||||
This example leverages a multi-stage Docker build to automatically compress
|
||||
your `.ged` file and `photos/` folder into a secure `.gdz` archive on-the-fly,
|
||||
preserving your image directory path structures.
|
||||
|
||||
## Structure
|
||||
|
||||
- Place your `family.ged` file here.
|
||||
- Place your photos also in this directory, or inside a `photos/` folder in
|
||||
this directory. If you put the photos in the `photos/` directory, make sure
|
||||
your GEDCOM file contains file references containing the `photos/` prefix.
|
||||
See the sample [family.ged](family.ged).
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Build your custom container:
|
||||
```bash
|
||||
docker build -t my-photo-tree .
|
||||
```
|
||||
````
|
||||
|
||||
2. Run your container:
|
||||
```bash
|
||||
docker run -d -p 8080:8080 my-photo-tree
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
- **Rationale**:
|
||||
- **Simple Template**: Demonstrates the standard, zero-friction path for users
|
||||
who just have a raw `.ged` file.
|
||||
- **Photos Template (On-The-Fly Zipper)**: Solves the problem of executing
|
||||
commands in a shell-less, commandless distroless container. By spinning up a
|
||||
lightweight Alpine zipper image to execute the native `zip` tool, and
|
||||
copying _only_ the finished `.gdz` artifact into the final stage, we achieve
|
||||
a completely self-contained, secure target image.
|
||||
- **Preserved Path Zip File Structure**: Packaging photos by zipping from the
|
||||
build root (`zip -r family.gdz family.ged photos/`) preserves the exact
|
||||
folder hierarchy relative to the GEDCOM. This guarantees that any complex or
|
||||
structured media folders (e.g., `photos/1990s/wedding.jpg`) match the exact
|
||||
file references declared inside the GEDCOM file, avoiding broken images from
|
||||
flattened zip scopes.
|
||||
|
||||
@@ -2,86 +2,224 @@
|
||||
|
||||
## Problem
|
||||
|
||||
Topola Viewer renders an interactive graphical family tree centered on a selected individual, displaying connected nodes for ancestors, descendants, and spouses. However, as family structures expand or users zoom in for detail, directly related family members frequently flow off the visible screen boundaries, requiring tedious canvas manipulation to locate and select them. To improve navigation efficiency and provide clear relational context at a glance, the application needs a dedicated layout block in the side panel that consolidates and displays the focused person's parents, spouses, and children. Providing these primary relationships as direct clickable links ensures users can rapidly transition focus between immediate family members regardless of their current viewport position on the graphical canvas.
|
||||
Topola Viewer renders an interactive graphical family tree centered on a
|
||||
selected individual, displaying connected nodes for ancestors, descendants, and
|
||||
spouses. However, as family structures expand or users zoom in for detail,
|
||||
directly related family members frequently flow off the visible screen
|
||||
boundaries, requiring tedious canvas manipulation to locate and select them. To
|
||||
improve navigation efficiency and provide clear relational context at a glance,
|
||||
the application needs a dedicated layout block in the side panel that
|
||||
consolidates and displays the focused person's parents, spouses, and children.
|
||||
Providing these primary relationships as direct clickable links ensures users
|
||||
can rapidly transition focus between immediate family members regardless of
|
||||
their current viewport position on the graphical canvas.
|
||||
|
||||
## Technical Plan
|
||||
|
||||
To introduce the immediate family view without disrupting existing features, the implementation introduces a focused visual component that acts as an intermediary between the raw genealogy data and the user interface.
|
||||
To introduce the immediate family view without disrupting existing features, the
|
||||
implementation introduces a focused visual component that acts as an
|
||||
intermediary between the raw genealogy data and the user interface.
|
||||
|
||||
The system relies on three primary components working together:
|
||||
1. **Genealogy Data Store**: The core repository containing all parsed individuals and family records. It provides lookup utilities to resolve cross-references between children and their parents or spouses.
|
||||
2. **Immediate Family Component**: A dedicated new interface element embedded directly inside the side panel. It queries the data store for the currently selected person, extracts their primary set of parents, and groups all associated children under their respective spouses or partners.
|
||||
3. **Navigation Module**: When a user clicks on any parent, spouse, or child link within the new block, this module updates the application's active web address with the chosen relative's identifier. This action instantly re-centers both the side panel and the primary visual tree onto the newly selected family member.
|
||||
|
||||
1. **Genealogy Data Store**: The core repository containing all parsed
|
||||
individuals and family records. It provides lookup utilities to resolve
|
||||
cross-references between children and their parents or spouses.
|
||||
2. **Immediate Family Component**: A dedicated new interface element embedded
|
||||
directly inside the side panel. It queries the data store for the currently
|
||||
selected person, extracts their primary set of parents, and groups all
|
||||
associated children under their respective spouses or partners.
|
||||
3. **Navigation Module**: When a user clicks on any parent, spouse, or child
|
||||
link within the new block, this module updates the application's active web
|
||||
address with the chosen relative's identifier. This action instantly
|
||||
re-centers both the side panel and the primary visual tree onto the newly
|
||||
selected family member.
|
||||
|
||||
## Alternatives
|
||||
|
||||
During the initial design discussions, several alternative implementation strategies were evaluated and ultimately ruled out to ensure optimal user experience and maintainability:
|
||||
During the initial design discussions, several alternative implementation
|
||||
strategies were evaluated and ultimately ruled out to ensure optimal user
|
||||
experience and maintainability:
|
||||
|
||||
### 1. Integrating Relationships into the Event Timeline
|
||||
* **Concept**: Interweave parents and children directly into the chronologically sorted lifecycle event list (e.g., rendering children as "Child Born" timeline events and placing parents near the target person's birth event).
|
||||
* **Reason for Rejection**: Structural relationships are fundamentally distinct from point-in-time events. Forcing them into an event-driven layout scatters family members across the full vertical height of the panel, creating severe timeline clutter and undermining the core goal of rapid navigation.
|
||||
|
||||
- **Concept**: Interweave parents and children directly into the chronologically
|
||||
sorted lifecycle event list (e.g., rendering children as "Child Born" timeline
|
||||
events and placing parents near the target person's birth event).
|
||||
- **Reason for Rejection**: Structural relationships are fundamentally distinct
|
||||
from point-in-time events. Forcing them into an event-driven layout scatters
|
||||
family members across the full vertical height of the panel, creating severe
|
||||
timeline clutter and undermining the core goal of rapid navigation.
|
||||
|
||||
### 2. Removing the Timeline Spouse Link
|
||||
* **Concept**: Strip out the standalone spouse navigation link from the chronological marriage event entries to prevent duplicating the link in both the timeline and the new Immediate Family block.
|
||||
* **Reason for Rejection**: Detaching the spouse link from the timeline breaks historical context, separating the marriage ceremony record from the actual partner involved. Maintaining both preserves event completeness while establishing a centralized navigation block.
|
||||
|
||||
- **Concept**: Strip out the standalone spouse navigation link from the
|
||||
chronological marriage event entries to prevent duplicating the link in both
|
||||
the timeline and the new Immediate Family block.
|
||||
- **Reason for Rejection**: Detaching the spouse link from the timeline breaks
|
||||
historical context, separating the marriage ceremony record from the actual
|
||||
partner involved. Maintaining both preserves event completeness while
|
||||
establishing a centralized navigation block.
|
||||
|
||||
### 3. Rendering a Flat List of Children
|
||||
* **Concept**: Consolidate all children into a single flat list ordered strictly by birth date, omitting spousal boundaries.
|
||||
* **Reason for Rejection**: Fails to represent blended families clearly. Explicitly grouping children under their respective spouse or partner headers makes full-sibling versus half-sibling structures immediately obvious to the user.
|
||||
|
||||
- **Concept**: Consolidate all children into a single flat list ordered strictly
|
||||
by birth date, omitting spousal boundaries.
|
||||
- **Reason for Rejection**: Fails to represent blended families clearly.
|
||||
Explicitly grouping children under their respective spouse or partner headers
|
||||
makes full-sibling versus half-sibling structures immediately obvious to the
|
||||
user.
|
||||
|
||||
### 4. Uniformly Showing or Hiding Unknown Spouses
|
||||
* **Concept**: Apply a blanket rule to either always display an "Unknown Spouse" header for single-parent records or entirely hide unknown headers across all scenarios.
|
||||
* **Reason for Rejection**: Always showing the header adds unnecessary visual noise to standard single-family layouts. Always hiding it obscures complex relationship boundaries when an individual has children across multiple partners where some partner names are unrecorded. The conditional approach dynamically cleans up single-family displays while preserving vital structural boundaries for multi-partner families.
|
||||
|
||||
- **Concept**: Apply a blanket rule to either always display an "Unknown Spouse"
|
||||
header for single-parent records or entirely hide unknown headers across all
|
||||
scenarios.
|
||||
- **Reason for Rejection**: Always showing the header adds unnecessary visual
|
||||
noise to standard single-family layouts. Always hiding it obscures complex
|
||||
relationship boundaries when an individual has children across multiple
|
||||
partners where some partner names are unrecorded. The conditional approach
|
||||
dynamically cleans up single-family displays while preserving vital structural
|
||||
boundaries for multi-partner families.
|
||||
|
||||
### 5. Supporting Multiple Sets of Parents Immediately
|
||||
* **Concept**: Query all associated `FAMC` records to display biological, adoptive, and foster parent sets side-by-side from day one.
|
||||
* **Reason for Rejection**: Introduces substantial UI complexity for labeling pedigree types (`PEDI` sub-tags) and requires wider structural refactoring of active focus state handling across the core canvas layout. Deferring to a single primary parent set keeps the initial feature scope robust and achievable.
|
||||
|
||||
- **Concept**: Query all associated `FAMC` records to display biological,
|
||||
adoptive, and foster parent sets side-by-side from day one.
|
||||
- **Reason for Rejection**: Introduces substantial UI complexity for labeling
|
||||
pedigree types (`PEDI` sub-tags) and requires wider structural refactoring of
|
||||
active focus state handling across the core canvas layout. Deferring to a
|
||||
single primary parent set keeps the initial feature scope robust and
|
||||
achievable.
|
||||
|
||||
## Detailed Implementation
|
||||
|
||||
Executing this feature requires clean extensions across the side panel architecture. To maintain high code quality and ensure strict boundaries, the implementation avoids modifying shared core state or event logic directly, instead isolating rendering responsibilities into modular layers.
|
||||
Executing this feature requires clean extensions across the side panel
|
||||
architecture. To maintain high code quality and ensure strict boundaries, the
|
||||
implementation avoids modifying shared core state or event logic directly,
|
||||
instead isolating rendering responsibilities into modular layers.
|
||||
|
||||
Below is the exhaustive file-by-file breakdown enumerating every file that will be created or modified, accompanied by detailed implementation step guidelines and specific technical rationale.
|
||||
Below is the exhaustive file-by-file breakdown enumerating every file that will
|
||||
be created or modified, accompanied by detailed implementation step guidelines
|
||||
and specific technical rationale.
|
||||
|
||||
### 1. Component Creation
|
||||
|
||||
#### [NEW] [immediate-family.tsx](../src/sidepanel/details/immediate-family.tsx)
|
||||
* **Rationale**: Encapsulating the new feature inside a dedicated component file separates concerns, avoids bloating the orchestration component ([details.tsx](../src/sidepanel/details/details.tsx)), and centralizes specialized rendering logic for single parent sets, childless spouses, conditional unknown headers, and chronologically sorted child groupings.
|
||||
* **Implementation Steps**:
|
||||
1. **Imports**: Import React elements, Semantic UI layout wrappers (`Item`, `Header`), navigation routing dependencies (`Link`, `useLocation` from `react-router`, `query-string`), localization wrappers (`FormattedMessage`, `useIntl` from `react-intl`), and GEDCOM utility functions from `../../util/gedcom_util` ([dereference](../src/util/gedcom_util.ts#L171-L183), [getName](../src/util/gedcom_util.ts#L292-L302), [pointerToId](../src/util/gedcom_util.ts#L42-L44), [resolveDate](../src/util/gedcom_util.ts#L339-L342)).
|
||||
2. **Relative Link Helper**: Define an internal component (e.g., `RelativeLink`) that renders an individual relative profile as a `<Link>`. It extracts the target relative's plain ID directly from the raw sub-entry string via `pointerToId(subEntry.data)` before dereferencing to prevent runtime crashes on broken references. It parses `location.search` using `useLocation()`, sets the `indi` query parameter to the target ID, stringifies the search parameters, routes to `/view`, and outputs the resolved display name via `getName` (falling back to localized unknown string if undefined).
|
||||
3. **Parents Block Renderer**: Implement a rendering method that extracts the active individual's tree array. Find the first sub-entry where `tag === 'FAMC'`. Guard against `undefined` if parents are unrecorded. Safely dereference this pointer against the repository's family mapping array to locate the parental family record. Scan the resolved family entry for all `HUSB` and `WIFE` sub-entries. Extract plain IDs via `pointerToId(subEntry.data)` and dereference each against the repository's individual mapping array to resolve profile records. Conditionally output localized sub-headers (e.g., "Father", "Mother") accompanied by clickable `RelativeLink` instances only for present parent records.
|
||||
4. **Spouses and Children Block Renderer**: Implement a rendering method that iterates over all `FAMS` tags within the active individual's tree. Dereference each entry to fetch its corresponding family record. For each family record, extract the partner/spouse pointers (`HUSB` or `WIFE` tags ensuring `!subEntry.data.includes(props.indi)` to prevent extracting the focused person as their own spouse) and extract all child records (`CHIL`).
|
||||
5. **Child Record Dereferencing and Sorting**: Explicitly dereference each extracted `CHIL` sub-entry against `gedcom.indis` to access child profile records. Extract their birth dates using `resolveDate` and sort the children array chronologically by birth date before outputting to ensure correct relational flow.
|
||||
6. **Conditional Unknown Spouse Logic**: Prior to outputting headers, evaluate the total length of the mapped `FAMS` array. Guard against incomplete/empty family entries by suppressing groups containing neither a valid spouse pointer nor children. If a spouse profile pointer is absent and total `FAMS` equals `1`, suppress the spouse header output. If a spouse pointer is absent and total valid `FAMS` groups exceed `1`, output a visible "Spouse: Unknown" block header to clearly define half-sibling boundaries.
|
||||
7. **Sequential Group Output**: Output each valid spousal family group as an encapsulated sub-block. Render childless spouses as complete standalone items. Output the resolved, sorted children collection as relative links below their respective spouse/partner header.
|
||||
8. **Container Wrapper**: Export the primary `ImmediateFamily` component wrapper. If active parent, spouse, or child nodes exist, wrap the consolidated layout blocks inside a single Semantic UI `<Item>` container structured with `<Item.Content>`. If no immediate family members exist, return `null` to prevent empty dividers in the side panel DOM.
|
||||
|
||||
- **Rationale**: Encapsulating the new feature inside a dedicated component file
|
||||
separates concerns, avoids bloating the orchestration component
|
||||
([details.tsx](../src/sidepanel/details/details.tsx)), and centralizes
|
||||
specialized rendering logic for single parent sets, childless spouses,
|
||||
conditional unknown headers, and chronologically sorted child groupings.
|
||||
- **Implementation Steps**:
|
||||
1. **Imports**: Import React elements, Semantic UI layout wrappers (`Item`,
|
||||
`Header`), navigation routing dependencies (`Link`, `useLocation` from
|
||||
`react-router`, `query-string`), localization wrappers (`FormattedMessage`,
|
||||
`useIntl` from `react-intl`), and GEDCOM utility functions from
|
||||
`../../util/gedcom_util`
|
||||
([dereference](../src/util/gedcom_util.ts#L171-L183),
|
||||
[getName](../src/util/gedcom_util.ts#L292-L302),
|
||||
[pointerToId](../src/util/gedcom_util.ts#L42-L44),
|
||||
[resolveDate](../src/util/gedcom_util.ts#L339-L342)).
|
||||
2. **Relative Link Helper**: Define an internal component (e.g.,
|
||||
`RelativeLink`) that renders an individual relative profile as a `<Link>`.
|
||||
It extracts the target relative's plain ID directly from the raw sub-entry
|
||||
string via `pointerToId(subEntry.data)` before dereferencing to prevent
|
||||
runtime crashes on broken references. It parses `location.search` using
|
||||
`useLocation()`, sets the `indi` query parameter to the target ID,
|
||||
stringifies the search parameters, routes to `/view`, and outputs the
|
||||
resolved display name via `getName` (falling back to localized unknown
|
||||
string if undefined).
|
||||
3. **Parents Block Renderer**: Implement a rendering method that extracts the
|
||||
active individual's tree array. Find the first sub-entry where
|
||||
`tag === 'FAMC'`. Guard against `undefined` if parents are unrecorded.
|
||||
Safely dereference this pointer against the repository's family mapping
|
||||
array to locate the parental family record. Scan the resolved family entry
|
||||
for all `HUSB` and `WIFE` sub-entries. Extract plain IDs via
|
||||
`pointerToId(subEntry.data)` and dereference each against the repository's
|
||||
individual mapping array to resolve profile records. Conditionally output
|
||||
localized sub-headers (e.g., "Father", "Mother") accompanied by clickable
|
||||
`RelativeLink` instances only for present parent records.
|
||||
4. **Spouses and Children Block Renderer**: Implement a rendering method that
|
||||
iterates over all `FAMS` tags within the active individual's tree.
|
||||
Dereference each entry to fetch its corresponding family record. For each
|
||||
family record, extract the partner/spouse pointers (`HUSB` or `WIFE` tags
|
||||
ensuring `!subEntry.data.includes(props.indi)` to prevent extracting the
|
||||
focused person as their own spouse) and extract all child records (`CHIL`).
|
||||
5. **Child Record Dereferencing and Sorting**: Explicitly dereference each
|
||||
extracted `CHIL` sub-entry against `gedcom.indis` to access child profile
|
||||
records. Extract their birth dates using `resolveDate` and sort the
|
||||
children array chronologically by birth date before outputting to ensure
|
||||
correct relational flow.
|
||||
6. **Conditional Unknown Spouse Logic**: Prior to outputting headers, evaluate
|
||||
the total length of the mapped `FAMS` array. Guard against incomplete/empty
|
||||
family entries by suppressing groups containing neither a valid spouse
|
||||
pointer nor children. If a spouse profile pointer is absent and total
|
||||
`FAMS` equals `1`, suppress the spouse header output. If a spouse pointer
|
||||
is absent and total valid `FAMS` groups exceed `1`, output a visible
|
||||
"Spouse: Unknown" block header to clearly define half-sibling boundaries.
|
||||
7. **Sequential Group Output**: Output each valid spousal family group as an
|
||||
encapsulated sub-block. Render childless spouses as complete standalone
|
||||
items. Output the resolved, sorted children collection as relative links
|
||||
below their respective spouse/partner header.
|
||||
8. **Container Wrapper**: Export the primary `ImmediateFamily` component
|
||||
wrapper. If active parent, spouse, or child nodes exist, wrap the
|
||||
consolidated layout blocks inside a single Semantic UI `<Item>` container
|
||||
structured with `<Item.Content>`. If no immediate family members exist,
|
||||
return `null` to prevent empty dividers in the side panel DOM.
|
||||
|
||||
### 2. Orchestration Layer Modification
|
||||
|
||||
#### [MODIFY] [details.tsx](../src/sidepanel/details/details.tsx)
|
||||
* **Rationale**: Acts as the root layout orchestrator for the side panel info tab. Needs to import and render the new view block near the top of the component flow to guarantee primary relational links are visible above the fold without requiring users to scroll past massive event timelines.
|
||||
* **Implementation Steps**:
|
||||
|
||||
- **Rationale**: Acts as the root layout orchestrator for the side panel info
|
||||
tab. Needs to import and render the new view block near the top of the
|
||||
component flow to guarantee primary relational links are visible above the
|
||||
fold without requiring users to scroll past massive event timelines.
|
||||
- **Implementation Steps**:
|
||||
1. Import the `ImmediateFamily` component from `./immediate-family`.
|
||||
2. Locate the main [Details](../src/sidepanel/details/details.tsx#L338-L387) component export function.
|
||||
3. Identify the execution block rendering `{getSectionForEachMatchingEntry(entries, props.gedcom, ['OBJE'], imageDetails)}` (line 350-355).
|
||||
4. Directly below this block, and strictly *before* the `<Events>` timeline rendering execution line, embed `<ImmediateFamily gedcom={props.gedcom} indi={props.indi} />` directly within the `<Item.Group divided>` flow. Rely on the component's internal conditional logic to render the outer item bounds.
|
||||
2. Locate the main [Details](../src/sidepanel/details/details.tsx#L338-L387)
|
||||
component export function.
|
||||
3. Identify the execution block rendering
|
||||
`{getSectionForEachMatchingEntry(entries, props.gedcom, ['OBJE'], imageDetails)}`
|
||||
(line 350-355).
|
||||
4. Directly below this block, and strictly _before_ the `<Events>` timeline
|
||||
rendering execution line, embed
|
||||
`<ImmediateFamily gedcom={props.gedcom} indi={props.indi} />` directly
|
||||
within the `<Item.Group divided>` flow. Rely on the component's internal
|
||||
conditional logic to render the outer item bounds.
|
||||
|
||||
### 3. Registry and Documentation Updates
|
||||
|
||||
#### [MODIFY] [README.md](../src/sidepanel/details/README.md)
|
||||
* **Rationale**: Functions as the official index cataloging all available component views within the side panel details sub-package. Documenting the new file preserves architectural readability.
|
||||
* **Implementation Steps**:
|
||||
|
||||
- **Rationale**: Functions as the official index cataloging all available
|
||||
component views within the side panel details sub-package. Documenting the new
|
||||
file preserves architectural readability.
|
||||
- **Implementation Steps**:
|
||||
1. Locate the alphabetical files registry list.
|
||||
2. Insert a descriptive entry for `immediate-family.tsx`, explaining its role as a dedicated side panel module for grouping and displaying parents, spouses, and children as rapid-navigation links.
|
||||
2. Insert a descriptive entry for `immediate-family.tsx`, explaining its role
|
||||
as a dedicated side panel module for grouping and displaying parents,
|
||||
spouses, and children as rapid-navigation links.
|
||||
|
||||
### 4. Localization Contracts
|
||||
|
||||
#### [MODIFY] Translation JSON Files (`src/translations/*.json`)
|
||||
* **Rationale**: Guarantees that every new UI header label introduced by the feature supports multi-language localization cleanly across all supported international environments.
|
||||
* **Implementation Steps**:
|
||||
1. Establish explicit string descriptor keys to be mapped across all JSON locale files (e.g., `src/translations/en.json`, `de.json`, `pl.json`, `fr.json`, `it.json`, etc.) to avoid production fallback leakage:
|
||||
* `"family.immediate_family": "Immediate Family"`
|
||||
* `"family.parents": "Parents"`
|
||||
* `"family.father": "Father"`
|
||||
* `"family.mother": "Mother"`
|
||||
* `"family.spouse": "Spouse"`
|
||||
* `"family.unknown_spouse": "Unknown Spouse"`
|
||||
* `"family.children": "Children"`
|
||||
2. Ensure corresponding `<FormattedMessage>` invocations inside `immediate-family.tsx` reference these specific string descriptor keys.
|
||||
|
||||
- **Rationale**: Guarantees that every new UI header label introduced by the
|
||||
feature supports multi-language localization cleanly across all supported
|
||||
international environments.
|
||||
- **Implementation Steps**:
|
||||
1. Establish explicit string descriptor keys to be mapped across all JSON
|
||||
locale files (e.g., `src/translations/en.json`, `de.json`, `pl.json`,
|
||||
`fr.json`, `it.json`, etc.) to avoid production fallback leakage:
|
||||
- `"family.immediate_family": "Immediate Family"`
|
||||
- `"family.parents": "Parents"`
|
||||
- `"family.father": "Father"`
|
||||
- `"family.mother": "Mother"`
|
||||
- `"family.spouse": "Spouse"`
|
||||
- `"family.unknown_spouse": "Unknown Spouse"`
|
||||
- `"family.children": "Children"`
|
||||
2. Ensure corresponding `<FormattedMessage>` invocations inside
|
||||
`immediate-family.tsx` reference these specific string descriptor keys.
|
||||
|
||||
@@ -1,273 +1,530 @@
|
||||
# Load from Google Drive
|
||||
|
||||
## Problem Statement
|
||||
Currently, Topola Viewer operates primarily on local file uploads, external HTTP URLs, or integrations with APIs like WikiTree. However, users who maintain their genealogy files (such as `.ged` and `.gdz` files) on cloud storage platforms like Google Drive face significant friction when trying to view, share, or collaborate on their family trees. The lack of integrated cloud storage support makes sharing interactive trees between collaborators cumbersome, requiring them to manually download and re-upload files. Integrating Google Drive directly into Topola Viewer as a secure, read-only storage provider will allow users to seamlessly load their trees from the cloud and easily collaborate using direct shared links.
|
||||
|
||||
Currently, Topola Viewer operates primarily on local file uploads, external HTTP
|
||||
URLs, or integrations with APIs like WikiTree. However, users who maintain their
|
||||
genealogy files (such as `.ged` and `.gdz` files) on cloud storage platforms
|
||||
like Google Drive face significant friction when trying to view, share, or
|
||||
collaborate on their family trees. The lack of integrated cloud storage support
|
||||
makes sharing interactive trees between collaborators cumbersome, requiring them
|
||||
to manually download and re-upload files. Integrating Google Drive directly into
|
||||
Topola Viewer as a secure, read-only storage provider will allow users to
|
||||
seamlessly load their trees from the cloud and easily collaborate using direct
|
||||
shared links.
|
||||
|
||||
## The Technical Plan
|
||||
|
||||
To support Google Drive files, the application needs to orchestrate authentication, file selection, and download. The integration consists of five main components working together:
|
||||
To support Google Drive files, the application needs to orchestrate
|
||||
authentication, file selection, and download. The integration consists of five
|
||||
main components working together:
|
||||
|
||||
### Major Components
|
||||
|
||||
1. **Google Drive Service**: This is a helper component that manages the connection to Google's APIs. It loads the Google libraries, triggers the login popup, caches the authorization token, and opens the Google Picker file selector.
|
||||
2. **Google Drive Data Source**: A data loader that is registered in Topola's data source system. When the app is asked to load a file ID, this component uses the active authorization token to download the raw file content directly from Google Drive's secure servers.
|
||||
3. **App Router / Shell**: The main orchestrator of the application. It parses parameters from the URL (like `fileId` and `source=google-drive`) and initiates the loading process. If the file download fails due to access issues, it manages the state of the fallback dialog.
|
||||
4. **Access Authorization Modal**: A fallback dialog that displays when a user clicks a shared link to a Google Drive file that they do not yet have permission to view. It guides them to authenticate and select the target file using the Google Picker, which dynamically grants the app permission to access that file.
|
||||
5. **Google Drive Menu**: A button added to Topola Viewer's menu bar that allows users to connect their Google account and browse their Drive files.
|
||||
1. **Google Drive Service**: This is a helper component that manages the
|
||||
connection to Google's APIs. It loads the Google libraries, triggers the
|
||||
login popup, caches the authorization token, and opens the Google Picker file
|
||||
selector.
|
||||
2. **Google Drive Data Source**: A data loader that is registered in Topola's
|
||||
data source system. When the app is asked to load a file ID, this component
|
||||
uses the active authorization token to download the raw file content directly
|
||||
from Google Drive's secure servers.
|
||||
3. **App Router / Shell**: The main orchestrator of the application. It parses
|
||||
parameters from the URL (like `fileId` and `source=google-drive`) and
|
||||
initiates the loading process. If the file download fails due to access
|
||||
issues, it manages the state of the fallback dialog.
|
||||
4. **Access Authorization Modal**: A fallback dialog that displays when a user
|
||||
clicks a shared link to a Google Drive file that they do not yet have
|
||||
permission to view. It guides them to authenticate and select the target file
|
||||
using the Google Picker, which dynamically grants the app permission to
|
||||
access that file.
|
||||
5. **Google Drive Menu**: A button added to Topola Viewer's menu bar that allows
|
||||
users to connect their Google account and browse their Drive files.
|
||||
|
||||
## Alternatives Considered and Rejected
|
||||
|
||||
During the design phase, several alternative approaches were considered but rejected due to security, privacy, or complexity concerns:
|
||||
During the design phase, several alternative approaches were considered but
|
||||
rejected due to security, privacy, or complexity concerns:
|
||||
|
||||
### 1. Using the Broad `drive.readonly` Scope
|
||||
* **Alternative**: Requesting permission to read any file in the user's Google Drive so that shared links could be opened immediately without additional user interaction.
|
||||
* **Why Rejected**: Google classifies `drive.readonly` as a restricted scope. Publishing an app using this scope requires an annual third-party security assessment (CASA audit) which is prohibitively expensive for an open-source, non-profit project. Using the `drive.file` scope keeps the application within the non-sensitive tier while protecting user privacy by only granting access to files the user explicitly selects.
|
||||
|
||||
- **Alternative**: Requesting permission to read any file in the user's Google
|
||||
Drive so that shared links could be opened immediately without additional user
|
||||
interaction.
|
||||
- **Why Rejected**: Google classifies `drive.readonly` as a restricted scope.
|
||||
Publishing an app using this scope requires an annual third-party security
|
||||
assessment (CASA audit) which is prohibitively expensive for an open-source,
|
||||
non-profit project. Using the `drive.file` scope keeps the application within
|
||||
the non-sensitive tier while protecting user privacy by only granting access
|
||||
to files the user explicitly selects.
|
||||
|
||||
### 2. Anonymous Downloads for Publicly Shared Files
|
||||
* **Alternative**: Attempting to download files shared with "anyone with the link" directly using standard public URLs without requiring a Google sign-in.
|
||||
* **Why Rejected**: Public Google Drive URLs do not reliably support browser CORS headers for anonymous requests, and larger files trigger virus-warning pages that return HTML rather than raw file contents. Requiring a Google login for all Google Drive operations allows the app to fetch via the official API with an authorization token, which avoids both CORS and virus-scan issues and simplifies the code.
|
||||
|
||||
- **Alternative**: Attempting to download files shared with "anyone with the
|
||||
link" directly using standard public URLs without requiring a Google sign-in.
|
||||
- **Why Rejected**: Public Google Drive URLs do not reliably support browser
|
||||
CORS headers for anonymous requests, and larger files trigger virus-warning
|
||||
pages that return HTML rather than raw file contents. Requiring a Google login
|
||||
for all Google Drive operations allows the app to fetch via the official API
|
||||
with an authorization token, which avoids both CORS and virus-scan issues and
|
||||
simplifies the code.
|
||||
|
||||
### 3. Routing Google Drive Traffic Through a CORS Proxy
|
||||
* **Alternative**: Using the existing `topolaproxy.bieda.it` proxy to download files in order to bypass CORS restrictions.
|
||||
* **Why Rejected**: Routing traffic through a third-party proxy poses a severe security risk because it would require transmitting the user's Google OAuth access token to an external service. Additionally, family tree files contain sensitive personal information, and routing them through an unauthenticated proxy violates privacy-first principles. Direct client-side requests to `googleapis.com` ensure all traffic remains securely encrypted between the user's browser and Google.
|
||||
|
||||
- **Alternative**: Using the existing `topolaproxy.bieda.it` proxy to download
|
||||
files in order to bypass CORS restrictions.
|
||||
- **Why Rejected**: Routing traffic through a third-party proxy poses a severe
|
||||
security risk because it would require transmitting the user's Google OAuth
|
||||
access token to an external service. Additionally, family tree files contain
|
||||
sensitive personal information, and routing them through an unauthenticated
|
||||
proxy violates privacy-first principles. Direct client-side requests to
|
||||
`googleapis.com` ensure all traffic remains securely encrypted between the
|
||||
user's browser and Google.
|
||||
|
||||
### 4. Implementing Write/Edit Support
|
||||
* **Alternative**: Allowing users to modify their family trees and write back the changes directly to Google Drive.
|
||||
* **Why Rejected**: Topola Viewer is architected as a read-only visualization tool. Implementing full write support would require developing a robust GEDCOM and GEDZIP serialization engine, state mutation synchronization, and conflict resolution mechanisms. Limiting this feature to read-only viewing aligns with the application's core purpose and avoids significant, high-risk complexity.
|
||||
|
||||
- **Alternative**: Allowing users to modify their family trees and write back
|
||||
the changes directly to Google Drive.
|
||||
- **Why Rejected**: Topola Viewer is architected as a read-only visualization
|
||||
tool. Implementing full write support would require developing a robust GEDCOM
|
||||
and GEDZIP serialization engine, state mutation synchronization, and conflict
|
||||
resolution mechanisms. Limiting this feature to read-only viewing aligns with
|
||||
the application's core purpose and avoids significant, high-risk complexity.
|
||||
|
||||
## Detailed Implementation Plan
|
||||
|
||||
This section outlines every file that will be created or modified to implement the Google Drive storage feature, along with the technical rationale for each change.
|
||||
This section outlines every file that will be created or modified to implement
|
||||
the Google Drive storage feature, along with the technical rationale for each
|
||||
change.
|
||||
|
||||
---
|
||||
|
||||
### 1. Build and Dependencies Setup
|
||||
|
||||
#### [MODIFY] [package.json](../package.json)
|
||||
* **Rationale**: Type safety is critical for global variables introduced by Google scripts.
|
||||
* **Changes**: Add the following type definitions to `devDependencies`:
|
||||
* `@types/gapi` for the legacy Google API client.
|
||||
* `@types/google.picker` for the Google Picker API.
|
||||
* `@types/google.accounts` for the modern Google Identity Services (GIS) token client.
|
||||
|
||||
*Note: In contrast to the initial design where GAPI and GIS scripts were loaded statically in `index.html`, the final implementation loads these scripts dynamically on demand using the custom `loadScript()` helper function in `google_drive_service.ts` to improve performance and only download third-party assets when the user interacts with Google Drive.*
|
||||
- **Rationale**: Type safety is critical for global variables introduced by
|
||||
Google scripts.
|
||||
- **Changes**: Add the following type definitions to `devDependencies`:
|
||||
- `@types/gapi` for the legacy Google API client.
|
||||
- `@types/google.picker` for the Google Picker API.
|
||||
- `@types/google.accounts` for the modern Google Identity Services (GIS) token
|
||||
client.
|
||||
|
||||
_Note: In contrast to the initial design where GAPI and GIS scripts were loaded
|
||||
statically in `index.html`, the final implementation loads these scripts
|
||||
dynamically on demand using the custom `loadScript()` helper function in
|
||||
`google_drive_service.ts` to improve performance and only download third-party
|
||||
assets when the user interacts with Google Drive._
|
||||
|
||||
---
|
||||
|
||||
### 2. Data Source Layer
|
||||
|
||||
#### [MODIFY] [src/datasource/data_source.ts](../src/datasource/data_source.ts)
|
||||
* **Rationale**: Topola Viewer abstracts data loading using the `DataSource` interface and `DataSourceEnum`. We must register the Google Drive type here.
|
||||
* **Changes**: Add a `GOOGLE_DRIVE` entry to the `DataSourceEnum` export.
|
||||
|
||||
- **Rationale**: Topola Viewer abstracts data loading using the `DataSource`
|
||||
interface and `DataSourceEnum`. We must register the Google Drive type here.
|
||||
- **Changes**: Add a `GOOGLE_DRIVE` entry to the `DataSourceEnum` export.
|
||||
|
||||
#### [MODIFY] [src/datasource/load_data.ts](../src/datasource/load_data.ts)
|
||||
* **Rationale**: Restructure file loading logic so that Google Drive datasource can parse file content and perform initial caching without code duplication.
|
||||
* **Changes**: Export a new helper function, `loadAndPrepareFile(blob: Blob, cacheId: string): Promise<TopolaData>`, which calls `loadFile()` and `prepareData()` inside a try-catch block, handling `revokeObjectUrls()` in case of parsing errors.
|
||||
|
||||
- **Rationale**: Restructure file loading logic so that Google Drive datasource
|
||||
can parse file content and perform initial caching without code duplication.
|
||||
- **Changes**: Export a new helper function,
|
||||
`loadAndPrepareFile(blob: Blob, cacheId: string): Promise<TopolaData>`, which
|
||||
calls `loadFile()` and `prepareData()` inside a try-catch block, handling
|
||||
`revokeObjectUrls()` in case of parsing errors.
|
||||
|
||||
#### [NEW] [src/datasource/google_drive.ts](../src/datasource/google_drive.ts)
|
||||
* **Rationale**: Defines the logic for checking and loading Google Drive files.
|
||||
* **Changes**:
|
||||
* Define the interface `GoogleDriveSourceSpec` that contains the `fileId` and a `source` field mapped to `DataSourceEnum.GOOGLE_DRIVE`.
|
||||
* Create `GoogleDriveAuthError` (extends `Error`) to signal authentication failures or access denial back to the App controller.
|
||||
* Implement the `GoogleDriveDataSource` class matching the `DataSource` interface. The `loadData` function:
|
||||
1. Checks `sessionStorage` under `google-drive:{fileId}` to see if the parsed file is already cached (avoiding network requests on page refresh).
|
||||
2. If not cached, gets the active access token from `googleDriveService`. If missing or expired, throws `GoogleDriveAuthError`.
|
||||
3. Executes a direct `fetch` to Google's REST endpoint (`https://www.googleapis.com/drive/v3/files/{fileId}?alt=media`) using the token.
|
||||
4. Inspects response status: if 401, 403, or 404, throws `GoogleDriveAuthError`; if any other non-200 code, throws a standard `Error`.
|
||||
5. Parses the downloaded blob using `loadFile` (reusing existing GEDCOM/GEDZIP zip parsing).
|
||||
|
||||
- **Rationale**: Defines the logic for checking and loading Google Drive files.
|
||||
- **Changes**:
|
||||
- Define the interface `GoogleDriveSourceSpec` that contains the `fileId` and
|
||||
a `source` field mapped to `DataSourceEnum.GOOGLE_DRIVE`.
|
||||
- Create `GoogleDriveAuthError` (extends `Error`) to signal authentication
|
||||
failures or access denial back to the App controller.
|
||||
- Implement the `GoogleDriveDataSource` class matching the `DataSource`
|
||||
interface. The `loadData` function:
|
||||
1. Checks `sessionStorage` under `google-drive:{fileId}` to see if the
|
||||
parsed file is already cached (avoiding network requests on page
|
||||
refresh).
|
||||
2. If not cached, gets the active access token from `googleDriveService`. If
|
||||
missing or expired, throws `GoogleDriveAuthError`.
|
||||
3. Executes a direct `fetch` to Google's REST endpoint
|
||||
(`https://www.googleapis.com/drive/v3/files/{fileId}?alt=media`) using
|
||||
the token.
|
||||
4. Inspects response status: if 401, 403, or 404, throws
|
||||
`GoogleDriveAuthError`; if any other non-200 code, throws a standard
|
||||
`Error`.
|
||||
5. Parses the downloaded blob using `loadFile` (reusing existing
|
||||
GEDCOM/GEDZIP zip parsing).
|
||||
|
||||
#### [NEW] [src/datasource/google_drive_service.ts](../src/datasource/google_drive_service.ts)
|
||||
* **Rationale**: Keeps all interactions with Google Identity Services and GAPI initialization isolated, protecting the core app from dependency leakage.
|
||||
* **Changes**:
|
||||
* Create a singleton `googleDriveService` class.
|
||||
* Implement an initialization method `init()` that dynamically loads GAPI and GIS scripts using a helper function `loadScript(src)`. It then loads GAPI's `picker` library via `gapi.load('picker', ...)`. Access `window.gapi` and `window.google` using direct global references or `(window as any)` casting to prevent TypeScript compilation errors.
|
||||
* Handle OAuth initialization via `google.accounts.oauth2.initTokenClient` and store the access token in memory. Cache the token and its computed absolute expiration timestamp (`Date.now() + expires_in * 1000`) in `sessionStorage` to survive page refreshes in the same tab.
|
||||
* **Asynchronous Initialization**: Store the initialization Promise as `this.initPromise` (which resolves when global scripts are loaded and GAPI/GIS are initialized). All public service methods must await `this.initPromise` before executing to prevent race conditions and runtime errors.
|
||||
* **Promise-based OAuth Request**: Since `initTokenClient` accepts a single static callback, `requestToken` should store the current Promise's `resolve` and `reject` handlers on the service instance (`this.pendingAuthResolve` and `this.pendingAuthReject`) and invoke them inside the GIS callback.
|
||||
* Provide helper methods:
|
||||
* `getAccessToken()`: Returns the cached token if it has not expired (checked against the absolute expiration timestamp stored in `sessionStorage`).
|
||||
* `requestToken(forceAccountSelect)`: Returns a Promise that resolves when the Google Identity Services popup completes auth. If `forceAccountSelect` is true, sets the GIS configuration `prompt` parameter to `'select_account'` (critical for switching accounts on 403 errors).
|
||||
* `showPicker(onPicked, onCancel)`: Calculates display dimensions based on the window size (`Math.min` bounds) to ensure responsiveness on mobile, then builds and opens the Google Picker.
|
||||
* Set the developer API key via `.setDeveloperKey(import.meta.env.VITE_GOOGLE_API_KEY)` on the `PickerBuilder`.
|
||||
* Configure `DocsView` using `google.picker.ViewId.DOCS` and restrict it to files matching `.setMimeTypes('application/x-gedcom,text/vnd.familysearch.gedcom,application/x-zip')` to cover `.ged` and `.gdz` formats.
|
||||
* Set the origin using `.setOrigin(window.location.origin)` to prevent cross-origin issues between the Picker iframe and the viewer page.
|
||||
* In the Picker callback, check for `data.action === google.picker.Action.PICKED` and retrieve the file ID via `data[google.picker.Response.DOCUMENTS][0][google.picker.Document.ID]`, triggering `onPicked`.
|
||||
* If `data.action === google.picker.Action.CANCEL`, trigger the `onCancel` callback (if provided) to handle dialog closing gracefully.
|
||||
* `signOut()`: Revokes the token using `google.accounts.oauth2.revoke`, clears memory and `sessionStorage` tokens, and resets local state.
|
||||
|
||||
- **Rationale**: Keeps all interactions with Google Identity Services and GAPI
|
||||
initialization isolated, protecting the core app from dependency leakage.
|
||||
- **Changes**:
|
||||
- Create a singleton `googleDriveService` class.
|
||||
- Implement an initialization method `init()` that dynamically loads GAPI and
|
||||
GIS scripts using a helper function `loadScript(src)`. It then loads GAPI's
|
||||
`picker` library via `gapi.load('picker', ...)`. Access `window.gapi` and
|
||||
`window.google` using direct global references or `(window as any)` casting
|
||||
to prevent TypeScript compilation errors.
|
||||
- Handle OAuth initialization via `google.accounts.oauth2.initTokenClient` and
|
||||
store the access token in memory. Cache the token and its computed absolute
|
||||
expiration timestamp (`Date.now() + expires_in * 1000`) in `sessionStorage`
|
||||
to survive page refreshes in the same tab.
|
||||
- **Asynchronous Initialization**: Store the initialization Promise as
|
||||
`this.initPromise` (which resolves when global scripts are loaded and
|
||||
GAPI/GIS are initialized). All public service methods must await
|
||||
`this.initPromise` before executing to prevent race conditions and runtime
|
||||
errors.
|
||||
- **Promise-based OAuth Request**: Since `initTokenClient` accepts a single
|
||||
static callback, `requestToken` should store the current Promise's `resolve`
|
||||
and `reject` handlers on the service instance (`this.pendingAuthResolve` and
|
||||
`this.pendingAuthReject`) and invoke them inside the GIS callback.
|
||||
- Provide helper methods:
|
||||
- `getAccessToken()`: Returns the cached token if it has not expired
|
||||
(checked against the absolute expiration timestamp stored in
|
||||
`sessionStorage`).
|
||||
- `requestToken(forceAccountSelect)`: Returns a Promise that resolves when
|
||||
the Google Identity Services popup completes auth. If `forceAccountSelect`
|
||||
is true, sets the GIS configuration `prompt` parameter to
|
||||
`'select_account'` (critical for switching accounts on 403 errors).
|
||||
- `showPicker(onPicked, onCancel)`: Calculates display dimensions based on
|
||||
the window size (`Math.min` bounds) to ensure responsiveness on mobile,
|
||||
then builds and opens the Google Picker.
|
||||
- Set the developer API key via
|
||||
`.setDeveloperKey(import.meta.env.VITE_GOOGLE_API_KEY)` on the
|
||||
`PickerBuilder`.
|
||||
- Configure `DocsView` using `google.picker.ViewId.DOCS` and restrict it
|
||||
to files matching
|
||||
`.setMimeTypes('application/x-gedcom,text/vnd.familysearch.gedcom,application/x-zip')`
|
||||
to cover `.ged` and `.gdz` formats.
|
||||
- Set the origin using `.setOrigin(window.location.origin)` to prevent
|
||||
cross-origin issues between the Picker iframe and the viewer page.
|
||||
- In the Picker callback, check for
|
||||
`data.action === google.picker.Action.PICKED` and retrieve the file ID
|
||||
via
|
||||
`data[google.picker.Response.DOCUMENTS][0][google.picker.Document.ID]`,
|
||||
triggering `onPicked`.
|
||||
- If `data.action === google.picker.Action.CANCEL`, trigger the `onCancel`
|
||||
callback (if provided) to handle dialog closing gracefully.
|
||||
- `signOut()`: Revokes the token using `google.accounts.oauth2.revoke`,
|
||||
clears memory and `sessionStorage` tokens, and resets local state.
|
||||
|
||||
#### [NEW] [src/datasource/test_helpers.ts](../src/datasource/test_helpers.ts)
|
||||
* **Rationale**: Provides a mock `sessionStorage` utility for test environments.
|
||||
* **Changes**:
|
||||
* Define `mockSessionStorage()` which mocks `global.sessionStorage` with a key-value dictionary and returns it for inspection.
|
||||
|
||||
- **Rationale**: Provides a mock `sessionStorage` utility for test environments.
|
||||
- **Changes**:
|
||||
- Define `mockSessionStorage()` which mocks `global.sessionStorage` with a
|
||||
key-value dictionary and returns it for inspection.
|
||||
|
||||
#### [NEW] [tests/import_meta_transformer.js](../tests/import_meta_transformer.js)
|
||||
* **Rationale**: Translates Vite's `import.meta.env` to `process.env` so that tests running in Jest (a Node environment) can correctly access configuration properties.
|
||||
|
||||
- **Rationale**: Translates Vite's `import.meta.env` to `process.env` so that
|
||||
tests running in Jest (a Node environment) can correctly access configuration
|
||||
properties.
|
||||
|
||||
#### [MODIFY] [jest.config.ts](../jest.config.ts)
|
||||
* **Rationale**: Registers the custom `import_meta_transformer.js` for TS and TSX files.
|
||||
|
||||
- **Rationale**: Registers the custom `import_meta_transformer.js` for TS and
|
||||
TSX files.
|
||||
|
||||
#### [NEW] [src/datasource/google_drive.spec.ts](../src/datasource/google_drive.spec.ts)
|
||||
* **Rationale**: Unit testing for the `GoogleDriveDataSource` class, verifying that the data source is instantiated, fetches, and parses data correctly, handles cached items, and throws authentication errors when expected.
|
||||
|
||||
- **Rationale**: Unit testing for the `GoogleDriveDataSource` class, verifying
|
||||
that the data source is instantiated, fetches, and parses data correctly,
|
||||
handles cached items, and throws authentication errors when expected.
|
||||
|
||||
#### [NEW] [src/datasource/google_drive_service.spec.ts](../src/datasource/google_drive_service.spec.ts)
|
||||
* **Rationale**: Unit testing for the `GoogleDriveService` class including script loading, token caching, account selection, token revocation, and cache clearing.
|
||||
|
||||
- **Rationale**: Unit testing for the `GoogleDriveService` class including
|
||||
script loading, token caching, account selection, token revocation, and cache
|
||||
clearing.
|
||||
|
||||
---
|
||||
|
||||
### 3. User Interface Integration
|
||||
|
||||
#### [NEW] [src/menu/google_drive_menu.tsx](../src/menu/google_drive_menu.tsx)
|
||||
* **Rationale**: Provides the user entry point in the navigation bar to open files.
|
||||
* **Changes**:
|
||||
* Implement `GoogleDriveMenu` using the `MenuItem` abstraction.
|
||||
* When clicked, it requests an OAuth access token via `googleDriveService`. If successful, it launches the Google Picker.
|
||||
* When a file is selected, it extracts the ID and navigates to `/view` with search query params set to `source=google-drive&fileId={id}`.
|
||||
|
||||
- **Rationale**: Provides the user entry point in the navigation bar to open
|
||||
files.
|
||||
- **Changes**:
|
||||
- Implement `GoogleDriveMenu` using the `MenuItem` abstraction.
|
||||
- When clicked, it requests an OAuth access token via `googleDriveService`. If
|
||||
successful, it launches the Google Picker.
|
||||
- When a file is selected, it extracts the ID and navigates to `/view` with
|
||||
search query params set to `source=google-drive&fileId={id}`.
|
||||
|
||||
#### [MODIFY] [src/menu/top_bar.tsx](../src/menu/top_bar.tsx)
|
||||
* **Rationale**: TopBar contains open actions for uploads, URL loading, and WikiTree. We must inject the Google Drive options here.
|
||||
* **Changes**:
|
||||
* Add props: `onGoogleSignOut?: () => void`, `hasGoogleToken: boolean`, and `onGoogleTokenAcquired?: () => void`.
|
||||
* Check if `VITE_GOOGLE_CLIENT_ID` and `VITE_GOOGLE_API_KEY` exist in the build environment.
|
||||
* If present, render `GoogleDriveMenu` alongside other menus in the desktop and mobile file selectors.
|
||||
* Inject a **Google Drive Sign Out / Disconnect** button into the top bar in the same location where WikiTree login/logout options appear (on the right-aligned side of the menu for desktop, and list for mobile), rendering only if `hasGoogleToken` is true. Clicking it triggers a sign-out flow that:
|
||||
1. Calls the `onGoogleSignOut` callback.
|
||||
|
||||
- **Rationale**: TopBar contains open actions for uploads, URL loading, and
|
||||
WikiTree. We must inject the Google Drive options here.
|
||||
- **Changes**:
|
||||
- Add props: `onGoogleSignOut?: () => void`, `hasGoogleToken: boolean`, and
|
||||
`onGoogleTokenAcquired?: () => void`.
|
||||
- Check if `VITE_GOOGLE_CLIENT_ID` and `VITE_GOOGLE_API_KEY` exist in the
|
||||
build environment.
|
||||
- If present, render `GoogleDriveMenu` alongside other menus in the desktop
|
||||
and mobile file selectors.
|
||||
- Inject a **Google Drive Sign Out / Disconnect** button into the top bar in
|
||||
the same location where WikiTree login/logout options appear (on the
|
||||
right-aligned side of the menu for desktop, and list for mobile), rendering
|
||||
only if `hasGoogleToken` is true. Clicking it triggers a sign-out flow that:
|
||||
1. Calls the `onGoogleSignOut` callback.
|
||||
|
||||
#### [NEW] [src/menu/google_auth_modal.tsx](../src/menu/google_auth_modal.tsx)
|
||||
* **Rationale**: Essential for private file sharing support under `drive.file` and bypasses browser popup blockers.
|
||||
* **Changes**:
|
||||
* Define props interface:
|
||||
* `failedFileId: string`: The ID of the file that failed to load.
|
||||
* `onAuthSuccess: (fileId: string) => void`: Callback triggered when permission is successfully granted or a file is picked.
|
||||
* `onCancel: () => void`: Callback triggered when the user cancels the modal.
|
||||
* Create a modal view overlay displayed in the center of the screen.
|
||||
* Prompt: "Google Drive Access Required".
|
||||
* Provides a button to initiate connection. The modal implements a two-tier strategy tailored to `drive.file` constraints:
|
||||
1. Clicking the button first runs a quick OAuth request (`googleDriveService.requestToken()`) and attempts to download the target file ID. Due to `drive.file` limitations, this direct request will fail (with 403 or 404) on the first try if the app has not been authorized for this file yet in the user's Google Drive.
|
||||
2. In case of access failure (403/404), the modal UI will display detailed instructions guiding the user to select the file manually using the Google Picker. Selecting the file in the Picker explicitly grants the app permission to read it.
|
||||
3. **Shared Link Constraint**: If the file was shared via "Anyone with the link can view", it may not appear in the user's Picker search or "Shared with me" list until they open the link once in Google Drive or add a shortcut to "My Drive". The modal should document this instruction to guide users opening shared links.
|
||||
* **Popup Blocker Handling**: The OAuth popup is only triggered when the user explicitly clicks the "Connect" button (a direct user gesture). The modal must never attempt to open the OAuth popup automatically on mount.
|
||||
* **Non-blocking Loader**: When the login flow is pending, show a spinner on the button itself or a cancelable loader. Do not show an un-cancelable full-screen loading overlay, as Google Identity Services does not notify the app if the user closes the sign-in popup. The connect button must not be permanently disabled while loading; it should allow the user to click it again to retry or provide a manual reset/timeout in case they closed the login popup.
|
||||
* **Switch Account Button**: Provides a "Switch Account" button that allows users to authenticate with a different Google account directly from the modal dialog. In the implementation, this button is rendered conditionally once picker instructions are visible (after a failed direct download attempt).
|
||||
* **Cancel Action**: Provide a "Cancel" button. If clicked, calls `props.onCancel()` which redirects the user back to the homepage `/` (Intro).
|
||||
|
||||
- **Rationale**: Essential for private file sharing support under `drive.file`
|
||||
and bypasses browser popup blockers.
|
||||
- **Changes**:
|
||||
- Define props interface:
|
||||
- `failedFileId: string`: The ID of the file that failed to load.
|
||||
- `onAuthSuccess: (fileId: string) => void`: Callback triggered when
|
||||
permission is successfully granted or a file is picked.
|
||||
- `onCancel: () => void`: Callback triggered when the user cancels the
|
||||
modal.
|
||||
- Create a modal view overlay displayed in the center of the screen.
|
||||
- Prompt: "Google Drive Access Required".
|
||||
- Provides a button to initiate connection. The modal implements a two-tier
|
||||
strategy tailored to `drive.file` constraints:
|
||||
1. Clicking the button first runs a quick OAuth request
|
||||
(`googleDriveService.requestToken()`) and attempts to download the target
|
||||
file ID. Due to `drive.file` limitations, this direct request will fail
|
||||
(with 403 or 404) on the first try if the app has not been authorized for
|
||||
this file yet in the user's Google Drive.
|
||||
2. In case of access failure (403/404), the modal UI will display detailed
|
||||
instructions guiding the user to select the file manually using the
|
||||
Google Picker. Selecting the file in the Picker explicitly grants the app
|
||||
permission to read it.
|
||||
3. **Shared Link Constraint**: If the file was shared via "Anyone with the
|
||||
link can view", it may not appear in the user's Picker search or "Shared
|
||||
with me" list until they open the link once in Google Drive or add a
|
||||
shortcut to "My Drive". The modal should document this instruction to
|
||||
guide users opening shared links.
|
||||
- **Popup Blocker Handling**: The OAuth popup is only triggered when the user
|
||||
explicitly clicks the "Connect" button (a direct user gesture). The modal
|
||||
must never attempt to open the OAuth popup automatically on mount.
|
||||
- **Non-blocking Loader**: When the login flow is pending, show a spinner on
|
||||
the button itself or a cancelable loader. Do not show an un-cancelable
|
||||
full-screen loading overlay, as Google Identity Services does not notify the
|
||||
app if the user closes the sign-in popup. The connect button must not be
|
||||
permanently disabled while loading; it should allow the user to click it
|
||||
again to retry or provide a manual reset/timeout in case they closed the
|
||||
login popup.
|
||||
- **Switch Account Button**: Provides a "Switch Account" button that allows
|
||||
users to authenticate with a different Google account directly from the
|
||||
modal dialog. In the implementation, this button is rendered conditionally
|
||||
once picker instructions are visible (after a failed direct download
|
||||
attempt).
|
||||
- **Cancel Action**: Provide a "Cancel" button. If clicked, calls
|
||||
`props.onCancel()` which redirects the user back to the homepage `/`
|
||||
(Intro).
|
||||
|
||||
---
|
||||
|
||||
### 4. Main App Orchestration
|
||||
|
||||
#### [MODIFY] [src/app.tsx](../src/app.tsx)
|
||||
* **Rationale**: The central application container responsible for routing, state tracking, error displaying, and data orchestration.
|
||||
* **Changes**:
|
||||
* **Type & Registration**: Add `GoogleDriveSourceSpec` to the `DataSourceSpec` union. Instantiate `googleDriveDataSource` via `const googleDriveDataSource = new GoogleDriveDataSource();` and register it in both the `loadData()` and `isNewData()` switch statements in `app.tsx`.
|
||||
* **Credentials Check & Error Handling**: During data loading, if `source=google-drive` but `VITE_GOOGLE_CLIENT_ID` or `VITE_GOOGLE_API_KEY` is missing from the environment, throw an error signaling that Google Drive integration is not configured.
|
||||
* **"Open with" Query Parameter**: Add a root-level `useEffect` or route handler to inspect the `state` query parameter using React Router's `location.search` hook. If present:
|
||||
1. Wrap the query parsing logic in a `try/catch` block to handle cases where the `state` parameter is not valid JSON (e.g. from other OAuth providers).
|
||||
2. Check that the parsed JSON contains Google Drive specific keys (`action === 'open'` and a non-empty `ids` array) before triggering the redirect.
|
||||
3. If valid, extract the first file ID (`ids[0]`) from the array and ignore any other IDs. Perform a client-side redirect (soft navigate) to `/view?source=google-drive&fileId={fileId}` using `{ replace: true }` so it does not pollute the browser history and break the back button.
|
||||
4. Clear the `state` query parameter from the URL by performing the client-side redirect using `{ replace: true }`, which replaces the URL in the history and implicitly clears the `state` parameter to prevent infinite redirect loops on subsequent rendering and routing cycles.
|
||||
* **Argument Parsing**: Update `getArguments()` to support `source=google-drive` and `fileId` query params, returning a `GoogleDriveSourceSpec`.
|
||||
* **Google Auth State**: Add React state for showing the `<GoogleAuthModal />`, storing the `failedFileId`, and tracking `hasGoogleToken` (updated upon successful login or logout to ensure proper reactivity in `TopBar`).
|
||||
* **Catching Auth Errors**: Update the main `useEffect` data loading sequence. If `loadData()` throws `GoogleDriveAuthError`, do *not* set `state` to `AppState.ERROR` (which shows a scary red error banner). Instead, keep a clean background (e.g. keep `AppState.LOADING` or transition to a non-error background) and set `showAuthModal` to true.
|
||||
* **Resolving Auth Fallback (Avoiding Deadlock)**:
|
||||
* If the user selects a *new* file in the Picker, perform a soft `navigate` to update the URL parameters, triggering `isNewData` and initiating a normal reload.
|
||||
* If the user successfully authorizes and selects the *same* file ID (matching `failedFileId`), do *not* navigate (as the URL matches and would result in a no-op). Instead, explicitly reset `state` to `AppState.INITIAL`. This forces the `useEffect` to execute the load sequence again using the newly acquired access token.
|
||||
* If the user successfully selects the target file in the auth modal's callback, trigger url updating to refresh the page and successfully render the chart.
|
||||
* **Disconnect Callback**: Pass `onGoogleSignOut` to `TopBar` that:
|
||||
1. Revokes the token using `googleDriveService.signOut()`.
|
||||
2. Clears the active `data` and `selection` state in `App` and revokes all media Object URLs.
|
||||
3. Clears the active `sessionStorage` cache (all keys starting with `google-drive:`) to prevent unauthorized access by subsequent users on shared/public devices.
|
||||
4. Redirects the user back to the home route `/` (Intro).
|
||||
|
||||
- **Rationale**: The central application container responsible for routing,
|
||||
state tracking, error displaying, and data orchestration.
|
||||
- **Changes**:
|
||||
- **Type & Registration**: Add `GoogleDriveSourceSpec` to the `DataSourceSpec`
|
||||
union. Instantiate `googleDriveDataSource` via
|
||||
`const googleDriveDataSource = new GoogleDriveDataSource();` and register it
|
||||
in both the `loadData()` and `isNewData()` switch statements in `app.tsx`.
|
||||
- **Credentials Check & Error Handling**: During data loading, if
|
||||
`source=google-drive` but `VITE_GOOGLE_CLIENT_ID` or `VITE_GOOGLE_API_KEY`
|
||||
is missing from the environment, throw an error signaling that Google Drive
|
||||
integration is not configured.
|
||||
- **"Open with" Query Parameter**: Add a root-level `useEffect` or route
|
||||
handler to inspect the `state` query parameter using React Router's
|
||||
`location.search` hook. If present:
|
||||
1. Wrap the query parsing logic in a `try/catch` block to handle cases where
|
||||
the `state` parameter is not valid JSON (e.g. from other OAuth
|
||||
providers).
|
||||
2. Check that the parsed JSON contains Google Drive specific keys
|
||||
(`action === 'open'` and a non-empty `ids` array) before triggering the
|
||||
redirect.
|
||||
3. If valid, extract the first file ID (`ids[0]`) from the array and ignore
|
||||
any other IDs. Perform a client-side redirect (soft navigate) to
|
||||
`/view?source=google-drive&fileId={fileId}` using `{ replace: true }` so
|
||||
it does not pollute the browser history and break the back button.
|
||||
4. Clear the `state` query parameter from the URL by performing the
|
||||
client-side redirect using `{ replace: true }`, which replaces the URL in
|
||||
the history and implicitly clears the `state` parameter to prevent
|
||||
infinite redirect loops on subsequent rendering and routing cycles.
|
||||
- **Argument Parsing**: Update `getArguments()` to support
|
||||
`source=google-drive` and `fileId` query params, returning a
|
||||
`GoogleDriveSourceSpec`.
|
||||
- **Google Auth State**: Add React state for showing the
|
||||
`<GoogleAuthModal />`, storing the `failedFileId`, and tracking
|
||||
`hasGoogleToken` (updated upon successful login or logout to ensure proper
|
||||
reactivity in `TopBar`).
|
||||
- **Catching Auth Errors**: Update the main `useEffect` data loading sequence.
|
||||
If `loadData()` throws `GoogleDriveAuthError`, do _not_ set `state` to
|
||||
`AppState.ERROR` (which shows a scary red error banner). Instead, keep a
|
||||
clean background (e.g. keep `AppState.LOADING` or transition to a non-error
|
||||
background) and set `showAuthModal` to true.
|
||||
- **Resolving Auth Fallback (Avoiding Deadlock)**:
|
||||
- If the user selects a _new_ file in the Picker, perform a soft `navigate`
|
||||
to update the URL parameters, triggering `isNewData` and initiating a
|
||||
normal reload.
|
||||
- If the user successfully authorizes and selects the _same_ file ID
|
||||
(matching `failedFileId`), do _not_ navigate (as the URL matches and would
|
||||
result in a no-op). Instead, explicitly reset `state` to
|
||||
`AppState.INITIAL`. This forces the `useEffect` to execute the load
|
||||
sequence again using the newly acquired access token.
|
||||
- If the user successfully selects the target file in the auth modal's
|
||||
callback, trigger url updating to refresh the page and successfully render
|
||||
the chart.
|
||||
- **Disconnect Callback**: Pass `onGoogleSignOut` to `TopBar` that:
|
||||
1. Revokes the token using `googleDriveService.signOut()`.
|
||||
2. Clears the active `data` and `selection` state in `App` and revokes all
|
||||
media Object URLs.
|
||||
3. Clears the active `sessionStorage` cache (all keys starting with
|
||||
`google-drive:`) to prevent unauthorized access by subsequent users on
|
||||
shared/public devices.
|
||||
4. Redirects the user back to the home route `/` (Intro).
|
||||
|
||||
---
|
||||
|
||||
### 5. Localization Integration
|
||||
|
||||
#### [MODIFY] [src/translations/*.json](../src/translations) and [src/app.tsx](../src/app.tsx)
|
||||
* **Rationale**: Ensure all user-facing Google Drive integration UI strings are fully translated and localized.
|
||||
* **Changes**: Add translation keys for the new UI elements across all 7 localization files (`bg.json`, `cs.json`, `de.json`, `fr.json`, `it.json`, `pl.json`, `ru.json`). For the default English catalog, declare messages using the `defaultMessage` prop in components or as arguments in `intl.formatMessage` calls. Use a `TopolaError` with code `'GOOGLE_DRIVE_NOT_CONFIGURED'` to propagate configuration errors so they can be parsed and translated by `getI18nMessage()`.
|
||||
* `menu.load_from_google_drive`: "Load from Google Drive" (or language equivalent)
|
||||
* `menu.google_sign_out`: "Disconnect Google Drive" / "Sign out"
|
||||
* `google_auth.title`: "Google Drive Access Required"
|
||||
* `google_auth.instructions`: "To view this file, you must authenticate and select the file from your Google Drive to grant permissions."
|
||||
* `google_auth.grant_button`: "Grant Access & Select File"
|
||||
* `google_auth.cancel`: "Cancel"
|
||||
* `google_auth.picker_instructions_header`: "Permissions Required"
|
||||
* `google_auth.picker_instructions`: "The application does not have permission to read this file. Please select it in the file browser popup to grant access. If this is a shared file that doesn't show up, try adding a shortcut to your Drive first."
|
||||
* `google_auth.switch_account_button`: "Switch Account"
|
||||
* `error.GOOGLE_DRIVE_NOT_CONFIGURED`: "Google Drive integration is not configured."
|
||||
#### [MODIFY] [src/translations/\*.json](../src/translations) and [src/app.tsx](../src/app.tsx)
|
||||
|
||||
- **Rationale**: Ensure all user-facing Google Drive integration UI strings are
|
||||
fully translated and localized.
|
||||
- **Changes**: Add translation keys for the new UI elements across all 7
|
||||
localization files (`bg.json`, `cs.json`, `de.json`, `fr.json`, `it.json`,
|
||||
`pl.json`, `ru.json`). For the default English catalog, declare messages using
|
||||
the `defaultMessage` prop in components or as arguments in
|
||||
`intl.formatMessage` calls. Use a `TopolaError` with code
|
||||
`'GOOGLE_DRIVE_NOT_CONFIGURED'` to propagate configuration errors so they can
|
||||
be parsed and translated by `getI18nMessage()`.
|
||||
- `menu.load_from_google_drive`: "Load from Google Drive" (or language
|
||||
equivalent)
|
||||
- `menu.google_sign_out`: "Disconnect Google Drive" / "Sign out"
|
||||
- `google_auth.title`: "Google Drive Access Required"
|
||||
- `google_auth.instructions`: "To view this file, you must authenticate and
|
||||
select the file from your Google Drive to grant permissions."
|
||||
- `google_auth.grant_button`: "Grant Access & Select File"
|
||||
- `google_auth.cancel`: "Cancel"
|
||||
- `google_auth.picker_instructions_header`: "Permissions Required"
|
||||
- `google_auth.picker_instructions`: "The application does not have permission
|
||||
to read this file. Please select it in the file browser popup to grant
|
||||
access. If this is a shared file that doesn't show up, try adding a shortcut
|
||||
to your Drive first."
|
||||
- `google_auth.switch_account_button`: "Switch Account"
|
||||
- `error.GOOGLE_DRIVE_NOT_CONFIGURED`: "Google Drive integration is not
|
||||
configured."
|
||||
|
||||
---
|
||||
|
||||
## Google Cloud Platform Setup Guide
|
||||
|
||||
To support authentication and file picking, Topola Viewer must be connected to a Google Cloud Platform (GCP) project. Below are the step-by-step instructions to configure the GCP resources, credentials, and Workspace Marketplace listings.
|
||||
To support authentication and file picking, Topola Viewer must be connected to a
|
||||
Google Cloud Platform (GCP) project. Below are the step-by-step instructions to
|
||||
configure the GCP resources, credentials, and Workspace Marketplace listings.
|
||||
|
||||
### Step 1: Create a Google Cloud Project
|
||||
|
||||
1. Go to the [Google Cloud Console](https://console.cloud.google.com/).
|
||||
2. Click the project dropdown in the top navigation and select **New Project**.
|
||||
3. Enter a name (e.g., `Topola Viewer`) and click **Create**.
|
||||
|
||||
### Step 2: Enable Required APIs
|
||||
|
||||
1. Navigate to **APIs & Services** $\rightarrow$ **Library**.
|
||||
2. Search for and enable the following APIs:
|
||||
* **Google Drive API** (for file downloading).
|
||||
* **Google Picker API** (for the file browser popup).
|
||||
* **Google Workspace Marketplace SDK** (required to enable the "Open with" context menu inside Google Drive).
|
||||
- **Google Drive API** (for file downloading).
|
||||
- **Google Picker API** (for the file browser popup).
|
||||
- **Google Workspace Marketplace SDK** (required to enable the "Open with"
|
||||
context menu inside Google Drive).
|
||||
|
||||
### Step 3: Configure the OAuth Consent Screen
|
||||
|
||||
1. Navigate to **APIs & Services** $\rightarrow$ **OAuth consent screen**.
|
||||
2. Select **External** for User Type (so any Google user can log in) and click **Create**.
|
||||
3. Complete the **App information** (App name, Support email, Developer contact information) and click **Save and Continue**.
|
||||
2. Select **External** for User Type (so any Google user can log in) and click
|
||||
**Create**.
|
||||
3. Complete the **App information** (App name, Support email, Developer contact
|
||||
information) and click **Save and Continue**.
|
||||
4. In the **Scopes** step, click **Add or Remove Scopes**:
|
||||
* Add the scope: `https://www.googleapis.com/auth/drive.file`
|
||||
* This scope is classified as non-sensitive, meaning Topola Viewer does *not* require expensive security reviews or CASA audits to be verified by Google.
|
||||
- Add the scope: `https://www.googleapis.com/auth/drive.file`
|
||||
- This scope is classified as non-sensitive, meaning Topola Viewer does _not_
|
||||
require expensive security reviews or CASA audits to be verified by Google.
|
||||
5. Save and continue to finish the configuration.
|
||||
|
||||
### Step 4: Create OAuth 2.0 Credentials
|
||||
|
||||
1. Navigate to **APIs & Services** $\rightarrow$ **Credentials**.
|
||||
2. Click **Create Credentials** $\rightarrow$ **OAuth client ID**.
|
||||
3. Set the **Application type** to **Web application**.
|
||||
4. Under **Authorized JavaScript origins**, add the domains where the app is deployed, as well as local environments:
|
||||
* `https://pewu.github.io` (Production)
|
||||
* `https://apps.wikitree.com` (Wikitree Integration)
|
||||
* `http://localhost:3000` (Local testing and development)
|
||||
> [!IMPORTANT]
|
||||
> Google's OAuth validation is strict. In local development, ensure you access the application via `http://localhost:3000` rather than `http://127.0.0.1:3000`, as using the raw IP address will result in origin authorization failures unless `http://127.0.0.1:3000` is also explicitly added to this list.
|
||||
5. Click **Create**. Copy the generated **Client ID** to use in the app environment variables (`VITE_GOOGLE_CLIENT_ID`).
|
||||
4. Under **Authorized JavaScript origins**, add the domains where the app is
|
||||
deployed, as well as local environments:
|
||||
- `https://pewu.github.io` (Production)
|
||||
- `https://apps.wikitree.com` (Wikitree Integration)
|
||||
- `http://localhost:3000` (Local testing and development)
|
||||
> [!IMPORTANT] Google's OAuth validation is strict. In local development,
|
||||
> ensure you access the application via `http://localhost:3000` rather than
|
||||
> `http://127.0.0.1:3000`, as using the raw IP address will result in
|
||||
> origin authorization failures unless `http://127.0.0.1:3000` is also
|
||||
> explicitly added to this list.
|
||||
5. Click **Create**. Copy the generated **Client ID** to use in the app
|
||||
environment variables (`VITE_GOOGLE_CLIENT_ID`).
|
||||
|
||||
### Step 5: Create an API Key (for Google Picker)
|
||||
1. On the **Credentials** screen, click **Create Credentials** $\rightarrow$ **API key**.
|
||||
|
||||
1. On the **Credentials** screen, click **Create Credentials** $\rightarrow$
|
||||
**API key**.
|
||||
2. Edit the newly created API key to add restrictions:
|
||||
* **Application restrictions**: Choose **HTTP referrers (web sites)**.
|
||||
* Add the authorized referrers:
|
||||
* `https://pewu.github.io/topola/viewer/*`
|
||||
* `https://apps.wikitree.com/apps/wiech13/topola-viewer/*`
|
||||
* `http://localhost:3000/*`
|
||||
* **API restrictions**: Restrict the key to only allow requests to **both** the **Google Picker API** and the **Google Drive API** (the Picker component queries user files via the Drive API under the hood; restricting it strictly to the Picker API will cause requests to fail).
|
||||
3. Save the key. Copy the generated **API Key** to use in the app environment variables (`VITE_GOOGLE_API_KEY`).
|
||||
- **Application restrictions**: Choose **HTTP referrers (web sites)**.
|
||||
- Add the authorized referrers:
|
||||
- `https://pewu.github.io/topola/viewer/*`
|
||||
- `https://apps.wikitree.com/apps/wiech13/topola-viewer/*`
|
||||
- `http://localhost:3000/*`
|
||||
- **API restrictions**: Restrict the key to only allow requests to **both**
|
||||
the **Google Picker API** and the **Google Drive API** (the Picker
|
||||
component queries user files via the Drive API under the hood; restricting
|
||||
it strictly to the Picker API will cause requests to fail).
|
||||
3. Save the key. Copy the generated **API Key** to use in the app environment
|
||||
variables (`VITE_GOOGLE_API_KEY`).
|
||||
|
||||
### Step 6: Configure the Workspace Marketplace SDK ("Open with" Integration)
|
||||
1. Navigate to **APIs & Services** $\rightarrow$ **Enabled APIs & Services**, and select the **Google Workspace Marketplace SDK**.
|
||||
|
||||
1. Navigate to **APIs & Services** $\rightarrow$ **Enabled APIs & Services**,
|
||||
and select the **Google Workspace Marketplace SDK**.
|
||||
2. Click **App Integration** $\rightarrow$ **Configuration**.
|
||||
3. Enable the **Drive Extension** integration check.
|
||||
4. Configure the **Open URL**:
|
||||
* URL: `https://pewu.github.io/topola-viewer/` (or the corresponding deploy URL).
|
||||
- URL: `https://pewu.github.io/topola-viewer/` (or the corresponding deploy
|
||||
URL).
|
||||
5. Set up **File Handlers**:
|
||||
* Under **Default File Extensions**, register `.ged` and `.gdz`.
|
||||
6. (Optional) Configure the **Store Listing** to publish the extension to the public Google Workspace Marketplace, making it searchable and installable for anyone.
|
||||
- Under **Default File Extensions**, register `.ged` and `.gdz`.
|
||||
6. (Optional) Configure the **Store Listing** to publish the extension to the
|
||||
public Google Workspace Marketplace, making it searchable and installable for
|
||||
anyone.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Due to the dependency on third-party external Google APIs and credentials, testing this feature is divided into automated mocking (for CI environments and unit tests) and manual verification (for local development).
|
||||
Due to the dependency on third-party external Google APIs and credentials,
|
||||
testing this feature is divided into automated mocking (for CI environments and
|
||||
unit tests) and manual verification (for local development).
|
||||
|
||||
### 1. Automated Unit Testing
|
||||
|
||||
All automated unit tests are run via **Jest**. Since Node environments do not load Google's external CDN scripts, we must isolate and mock these dependencies.
|
||||
All automated unit tests are run via **Jest**. Since Node environments do not
|
||||
load Google's external CDN scripts, we must isolate and mock these dependencies.
|
||||
|
||||
#### Mocking Global Scripts
|
||||
Create a mock utility in the tests setup file to intercept calls to the global `google` and `gapi` interfaces:
|
||||
|
||||
Create a mock utility in the tests setup file to intercept calls to the global
|
||||
`google` and `gapi` interfaces:
|
||||
|
||||
```typescript
|
||||
global.gapi = {
|
||||
load: jest.fn((api: string, callback: () => void) => callback()),
|
||||
@@ -304,115 +561,200 @@ global.google = {
|
||||
```
|
||||
|
||||
#### Data Source Tests
|
||||
|
||||
In `src/datasource/google_drive.spec.ts` [NEW]:
|
||||
* **Verify `isNewData()`**: Confirm it returns true only when the `fileId` changes.
|
||||
* **Verify `loadData()` Success**: Mock `window.fetch` to return a mock blob, mock `getAccessToken()` to return a dummy token, and verify the data source parses the file stream correctly.
|
||||
* **Verify `loadData()` Auth Failure**: Mock `getAccessToken()` to return `null` or configure `window.fetch` to return `403 Forbidden`, and assert that `GoogleDriveAuthError` is thrown.
|
||||
|
||||
- **Verify `isNewData()`**: Confirm it returns true only when the `fileId`
|
||||
changes.
|
||||
- **Verify `loadData()` Success**: Mock `window.fetch` to return a mock blob,
|
||||
mock `getAccessToken()` to return a dummy token, and verify the data source
|
||||
parses the file stream correctly.
|
||||
- **Verify `loadData()` Auth Failure**: Mock `getAccessToken()` to return `null`
|
||||
or configure `window.fetch` to return `403 Forbidden`, and assert that
|
||||
`GoogleDriveAuthError` is thrown.
|
||||
|
||||
#### URL Parsing & Orchestration Tests
|
||||
|
||||
In existing test suites or manual validation:
|
||||
* Verify that URL query arguments containing `source=google-drive&fileId=XYZ` are parsed into the correct `GoogleDriveSourceSpec`.
|
||||
* Verify that if `loadData()` throws `GoogleDriveAuthError`, the application transitions state, avoids setting a global error message, and triggers the rendering of the `<GoogleAuthModal />` fallback popup.
|
||||
|
||||
- Verify that URL query arguments containing `source=google-drive&fileId=XYZ`
|
||||
are parsed into the correct `GoogleDriveSourceSpec`.
|
||||
- Verify that if `loadData()` throws `GoogleDriveAuthError`, the application
|
||||
transitions state, avoids setting a global error message, and triggers the
|
||||
rendering of the `<GoogleAuthModal />` fallback popup.
|
||||
|
||||
---
|
||||
|
||||
### 2. Manual Verification Checklist
|
||||
|
||||
Manual testing should be performed locally to verify the full OAuth loop and UI presentation:
|
||||
Manual testing should be performed locally to verify the full OAuth loop and UI
|
||||
presentation:
|
||||
|
||||
| Test Case | Action | Expected Result |
|
||||
| :--- | :--- | :--- |
|
||||
| **Missing Credentials** | Run the app with empty `VITE_GOOGLE_CLIENT_ID` / `VITE_GOOGLE_API_KEY` values. | The Google Drive option is completely hidden from the TopBar menus; local file upload and WikiTree operations continue working normally. |
|
||||
| **Picker Load** | Click "Load from Google Drive" in the menu, log in when prompted, select a `.ged`/`.gdz` file. | Google Picker popup closes, selected file is fetched directly from Google APIs, URL hash updates, and the family chart renders. |
|
||||
| **Shared Link Auth Fallback** | Load: `http://localhost:3000/#/view?source=google-drive&fileId=UNAUTHORIZED_ID` | The "Google Drive Access Required" modal overlays the screen, preventing rendering. |
|
||||
| **Grant Access Flow** | Click "Grant Access" in the fallback modal, authenticate, and pick the matching file from "Shared with me". | Modal closes, OAuth token gets authorized for the specific file ID, tree downloads and renders successfully. |
|
||||
| **Google Drive UI Open** | Navigate to: `http://localhost:3000/?state={"ids":["FILE_ID"],"action":"open"}` | The app detects the `state` param, automatically prompts Google authentication, downloads the file, and opens the chart. |
|
||||
| **Invalid Selection** | Pick a non-matching file from the fallback modal's picker. | App detects the mismatch, updates the URL to the new file's ID, downloads the newly picked file, and displays the tree. |
|
||||
| Test Case | Action | Expected Result |
|
||||
| :---------------------------- | :---------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Missing Credentials** | Run the app with empty `VITE_GOOGLE_CLIENT_ID` / `VITE_GOOGLE_API_KEY` values. | The Google Drive option is completely hidden from the TopBar menus; local file upload and WikiTree operations continue working normally. |
|
||||
| **Picker Load** | Click "Load from Google Drive" in the menu, log in when prompted, select a `.ged`/`.gdz` file. | Google Picker popup closes, selected file is fetched directly from Google APIs, URL hash updates, and the family chart renders. |
|
||||
| **Shared Link Auth Fallback** | Load: `http://localhost:3000/#/view?source=google-drive&fileId=UNAUTHORIZED_ID` | The "Google Drive Access Required" modal overlays the screen, preventing rendering. |
|
||||
| **Grant Access Flow** | Click "Grant Access" in the fallback modal, authenticate, and pick the matching file from "Shared with me". | Modal closes, OAuth token gets authorized for the specific file ID, tree downloads and renders successfully. |
|
||||
| **Google Drive UI Open** | Navigate to: `http://localhost:3000/?state={"ids":["FILE_ID"],"action":"open"}` | The app detects the `state` param, automatically prompts Google authentication, downloads the file, and opens the chart. |
|
||||
| **Invalid Selection** | Pick a non-matching file from the fallback modal's picker. | App detects the mismatch, updates the URL to the new file's ID, downloads the newly picked file, and displays the tree. |
|
||||
|
||||
---
|
||||
|
||||
### 3. Continuous Integration (CI) Safety
|
||||
|
||||
* In CI pipelines (e.g. GitHub Actions workflows), Google API keys are not present in the environment.
|
||||
* Because the feature checks for the presence of environment variables at compile/build time, the Google Drive menus are automatically omitted.
|
||||
* This ensures that visual regression tests (`npm run test:visual`) and E2E test runs (`npm run test:e2e`) pass without requiring live OAuth secrets or mock servers.
|
||||
- In CI pipelines (e.g. GitHub Actions workflows), Google API keys are not
|
||||
present in the environment.
|
||||
- Because the feature checks for the presence of environment variables at
|
||||
compile/build time, the Google Drive menus are automatically omitted.
|
||||
- This ensures that visual regression tests (`npm run test:visual`) and E2E test
|
||||
runs (`npm run test:e2e`) pass without requiring live OAuth secrets or mock
|
||||
servers.
|
||||
|
||||
---
|
||||
|
||||
## Build and Deployment CI/CD Configuration
|
||||
|
||||
To enable the Google Drive integration on the official public deployments, the Google API credentials must be injected during the automated build process in GitHub Actions.
|
||||
To enable the Google Drive integration on the official public deployments, the
|
||||
Google API credentials must be injected during the automated build process in
|
||||
GitHub Actions.
|
||||
|
||||
### Required GitHub Secrets
|
||||
You must configure the following repository secrets in your GitHub project settings:
|
||||
1. `GOOGLE_CLIENT_ID`: The official OAuth Client ID for `pewu.github.io` / `apps.wikitree.com`.
|
||||
|
||||
You must configure the following repository secrets in your GitHub project
|
||||
settings:
|
||||
|
||||
1. `GOOGLE_CLIENT_ID`: The official OAuth Client ID for `pewu.github.io` /
|
||||
`apps.wikitree.com`.
|
||||
2. `GOOGLE_API_KEY`: The official Google Picker API Key.
|
||||
|
||||
### Workflow Modifications
|
||||
|
||||
The build step (`npm run build`) in the deployment workflows must receive these secrets as environment variables.
|
||||
The build step (`npm run build`) in the deployment workflows must receive these
|
||||
secrets as environment variables.
|
||||
|
||||
#### 1. Deployment to GitHub Pages (`.github/workflows/deploy-gh-pages.yml`)
|
||||
|
||||
Pass the secrets to the build step:
|
||||
|
||||
```yaml
|
||||
- run: npm run build
|
||||
env:
|
||||
VITE_GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
VITE_GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
- run: npm run build
|
||||
env:
|
||||
VITE_GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
VITE_GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
```
|
||||
|
||||
#### 2. Deployment to WikiTree Apps (`.github/workflows/deploy-wikitree-apps.yml`)
|
||||
|
||||
Pass the secrets to the build step:
|
||||
|
||||
```yaml
|
||||
- run: npm run build
|
||||
env:
|
||||
VITE_GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
VITE_GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
- run: npm run build
|
||||
env:
|
||||
VITE_GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
VITE_GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
```
|
||||
|
||||
#### 3. Reusable Workflow Inheritance (`.github/workflows/deploy-everywhere.yml`)
|
||||
Enable secret inheritance on jobs calling these workflows to ensure secrets are propagated:
|
||||
|
||||
Enable secret inheritance on jobs calling these workflows to ensure secrets are
|
||||
propagated:
|
||||
|
||||
```yaml
|
||||
deploy-gh-pages:
|
||||
uses: ./.github/workflows/deploy-gh-pages.yml
|
||||
secrets: inherit
|
||||
deploy-gh-pages:
|
||||
uses: ./.github/workflows/deploy-gh-pages.yml
|
||||
secrets: inherit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security and Privacy
|
||||
|
||||
Since Topola Viewer is a purely client-side application that manages sensitive genealogical data, security and user privacy are core design pillars for this integration.
|
||||
Since Topola Viewer is a purely client-side application that manages sensitive
|
||||
genealogical data, security and user privacy are core design pillars for this
|
||||
integration.
|
||||
|
||||
### 1. Protection of Public API Credentials
|
||||
The application's client-side architecture requires compiling the public Google `Client ID` and `API Key` directly into the JavaScript bundle. These values are protected from abuse using Google Cloud Platform constraints:
|
||||
* **OAuth Domain Whitelisting**: The `Client ID` restricts **Authorized JavaScript Origins** and **Redirect URIs** to `https://pewu.github.io`, `https://apps.wikitree.com`, and `http://localhost:3000`. Google's authorization server will automatically block login attempts initiated from any other domains.
|
||||
* **API Key Restrictions**: The `API Key` enforces **HTTP Referrer Restrictions** matching the authorized domains. Additionally, the key is scoped via **API Restrictions** to only permit requests to the **Google Picker API** and **Google Drive API**, preventing its use on other Google Cloud services.
|
||||
* **No Secrets Compiled**: The application never utilizes or compiles an OAuth `Client Secret`, which is only required for server-side integrations.
|
||||
|
||||
The application's client-side architecture requires compiling the public Google
|
||||
`Client ID` and `API Key` directly into the JavaScript bundle. These values are
|
||||
protected from abuse using Google Cloud Platform constraints:
|
||||
|
||||
- **OAuth Domain Whitelisting**: The `Client ID` restricts **Authorized
|
||||
JavaScript Origins** and **Redirect URIs** to `https://pewu.github.io`,
|
||||
`https://apps.wikitree.com`, and `http://localhost:3000`. Google's
|
||||
authorization server will automatically block login attempts initiated from
|
||||
any other domains.
|
||||
- **API Key Restrictions**: The `API Key` enforces **HTTP Referrer
|
||||
Restrictions** matching the authorized domains. Additionally, the key is
|
||||
scoped via **API Restrictions** to only permit requests to the **Google Picker
|
||||
API** and **Google Drive API**, preventing its use on other Google Cloud
|
||||
services.
|
||||
- **No Secrets Compiled**: The application never utilizes or compiles an OAuth
|
||||
`Client Secret`, which is only required for server-side integrations.
|
||||
|
||||
### 2. Data Privacy & Zero-Server Architecture
|
||||
* **Direct Client-to-Google Communication**: All requests to Google Drive APIs are initiated directly from the user's browser.
|
||||
* **No Third-Party Proxies**: To prevent token leakage and maintain data confidentiality, Google Drive downloads **never** route through the `topolaproxy.bieda.it` CORS proxy.
|
||||
* **Local Parsing**: The fetched GEDCOM or GEDZIP file content is parsed entirely within the browser using local JavaScript. No family data, files, or access tokens are ever transmitted to, processed by, or stored on external servers.
|
||||
* **Short-Lived Auth Tokens**: The application uses short-lived access tokens that expire automatically. No persistent refresh tokens are requested or stored.
|
||||
* **Session Storage Cache Security**: To optimize load times, parsed tree data is cached in `sessionStorage` under `google-drive:{fileId}`. To protect user privacy on shared or public computers, signing out or disconnecting Google Drive explicitly purges all `google-drive:*` keys from the browser's `sessionStorage`.
|
||||
|
||||
- **Direct Client-to-Google Communication**: All requests to Google Drive APIs
|
||||
are initiated directly from the user's browser.
|
||||
- **No Third-Party Proxies**: To prevent token leakage and maintain data
|
||||
confidentiality, Google Drive downloads **never** route through the
|
||||
`topolaproxy.bieda.it` CORS proxy.
|
||||
- **Local Parsing**: The fetched GEDCOM or GEDZIP file content is parsed
|
||||
entirely within the browser using local JavaScript. No family data, files, or
|
||||
access tokens are ever transmitted to, processed by, or stored on external
|
||||
servers.
|
||||
- **Short-Lived Auth Tokens**: The application uses short-lived access tokens
|
||||
that expire automatically. No persistent refresh tokens are requested or
|
||||
stored.
|
||||
- **Session Storage Cache Security**: To optimize load times, parsed tree data
|
||||
is cached in `sessionStorage` under `google-drive:{fileId}`. To protect user
|
||||
privacy on shared or public computers, signing out or disconnecting Google
|
||||
Drive explicitly purges all `google-drive:*` keys from the browser's
|
||||
`sessionStorage`.
|
||||
|
||||
### 3. Principle of Least Privilege (OAuth Scopes)
|
||||
Instead of requesting the broad `drive.readonly` scope, which grants visibility into a user's entire Google Drive, this feature utilizes the restrictive `drive.file` scope. Under this scope:
|
||||
* The application only has permission to view files that the user has explicitly opened with the app (either by selecting the file in the Google Picker or opening it via the "Open with" Google Drive UI option).
|
||||
* Topola Viewer remains blind to all other files and directories in the user's Google Drive.
|
||||
|
||||
Instead of requesting the broad `drive.readonly` scope, which grants visibility
|
||||
into a user's entire Google Drive, this feature utilizes the restrictive
|
||||
`drive.file` scope. Under this scope:
|
||||
|
||||
- The application only has permission to view files that the user has explicitly
|
||||
opened with the app (either by selecting the file in the Google Picker or
|
||||
opening it via the "Open with" Google Drive UI option).
|
||||
- Topola Viewer remains blind to all other files and directories in the user's
|
||||
Google Drive.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Image Loading from Plain `.ged` Files**: Only `.gdz` archives are supported for viewing media files from Google Drive. Plain `.ged` files will only display genealogical data, and any relative/external image paths will render as broken images because standard HTML `<img>` tags cannot transmit the `Authorization` header required for private Google Drive requests.
|
||||
2. **Iframe Embedding (WikiTree)**: Running Google Drive authentication and loading files from Google Drive inside an embedding iframe (such as on `apps.wikitree.com`) can fail due to iframe popup sandboxing and origin mismatch restrictions. This configuration is not actively supported.
|
||||
3. **Session Refresh Cache**: When a page is refreshed, zipped images cached in `sessionStorage` might have invalid Object URLs. We choose to ignore this problem until it becomes a visible UX issue.
|
||||
4. **Access Modal Lockout**: If a user logs into a Google account that does not have permissions to read the file, they are prompted with the "Access Required" modal. Since this modal blocks the viewport, they cannot click the top-bar "Disconnect" button to switch Google accounts, requiring them to click "Cancel" to return to the homepage first.
|
||||
1. **Image Loading from Plain `.ged` Files**: Only `.gdz` archives are supported
|
||||
for viewing media files from Google Drive. Plain `.ged` files will only
|
||||
display genealogical data, and any relative/external image paths will render
|
||||
as broken images because standard HTML `<img>` tags cannot transmit the
|
||||
`Authorization` header required for private Google Drive requests.
|
||||
2. **Iframe Embedding (WikiTree)**: Running Google Drive authentication and
|
||||
loading files from Google Drive inside an embedding iframe (such as on
|
||||
`apps.wikitree.com`) can fail due to iframe popup sandboxing and origin
|
||||
mismatch restrictions. This configuration is not actively supported.
|
||||
3. **Session Refresh Cache**: When a page is refreshed, zipped images cached in
|
||||
`sessionStorage` might have invalid Object URLs. We choose to ignore this
|
||||
problem until it becomes a visible UX issue.
|
||||
4. **Access Modal Lockout**: If a user logs into a Google account that does not
|
||||
have permissions to read the file, they are prompted with the "Access
|
||||
Required" modal. Since this modal blocks the viewport, they cannot click the
|
||||
top-bar "Disconnect" button to switch Google accounts, requiring them to
|
||||
click "Cancel" to return to the homepage first.
|
||||
|
||||
---
|
||||
|
||||
## Future Improvements
|
||||
|
||||
1. **Self-Hosting Runtime Configuration Support**: Read `google-client-id` and `google-api-key` from HTML `<meta>` tags at runtime (similar to `topola-static-url`), allowing self-hosters to deploy and configure Google Drive support without rebuilding the static assets.
|
||||
2. **Fetch Cancellation & Race Condition Mitigation**: Implement an `AbortController` or active boolean flag cancellation check in `app.tsx`'s `useEffect` loader to ensure slow background downloads do not overwrite newer datasets if a user changes the source/file before the previous request finishes.
|
||||
|
||||
1. **Self-Hosting Runtime Configuration Support**: Read `google-client-id` and
|
||||
`google-api-key` from HTML `<meta>` tags at runtime (similar to
|
||||
`topola-static-url`), allowing self-hosters to deploy and configure Google
|
||||
Drive support without rebuilding the static assets.
|
||||
2. **Fetch Cancellation & Race Condition Mitigation**: Implement an
|
||||
`AbortController` or active boolean flag cancellation check in `app.tsx`'s
|
||||
`useEffect` loader to ensure slow background downloads do not overwrite newer
|
||||
datasets if a user changes the source/file before the previous request
|
||||
finishes.
|
||||
|
||||
@@ -2,13 +2,28 @@
|
||||
|
||||
## 1. Business & Functional Problem Statement
|
||||
|
||||
Topola Viewer currently relies on a Cypress end-to-end (E2E) test suite to verify critical user flows, such as interactive chart rendering, search capabilities, and experimental WebMCP integrations. However, the existing setup is brittle and hard to maintain because it depends on live, external internet connections to fetch remote GEDCOM trees, uses loosely-typed JavaScript spec files, and suffers from complex process orchestration. Additionally, Cypress's historical architectural limitations make testing cross-origin iframe structures in embedded mode slow and difficult to simulate locally. Migrating this testing suite to Playwright in TypeScript will establish a fast, fully type-safe, hermetic, and self-contained validation pipeline, ensuring absolute quality control and deployment confidence.
|
||||
Topola Viewer currently relies on a Cypress end-to-end (E2E) test suite to
|
||||
verify critical user flows, such as interactive chart rendering, search
|
||||
capabilities, and experimental WebMCP integrations. However, the existing setup
|
||||
is brittle and hard to maintain because it depends on live, external internet
|
||||
connections to fetch remote GEDCOM trees, uses loosely-typed JavaScript spec
|
||||
files, and suffers from complex process orchestration. Additionally, Cypress's
|
||||
historical architectural limitations make testing cross-origin iframe structures
|
||||
in embedded mode slow and difficult to simulate locally. Migrating this testing
|
||||
suite to Playwright in TypeScript will establish a fast, fully type-safe,
|
||||
hermetic, and self-contained validation pipeline, ensuring absolute quality
|
||||
control and deployment confidence.
|
||||
|
||||
## 2. The Technical Plan
|
||||
|
||||
The technical solution relies on transitioning our testing engine from Cypress to Playwright, utilizing a self-contained, local-first execution model. Instead of relying on external servers or separate terminal scripts to boot up our application, Playwright will orchestrate the entire lifecycle from start to finish.
|
||||
The technical solution relies on transitioning our testing engine from Cypress
|
||||
to Playwright, utilizing a self-contained, local-first execution model. Instead
|
||||
of relying on external servers or separate terminal scripts to boot up our
|
||||
application, Playwright will orchestrate the entire lifecycle from start to
|
||||
finish.
|
||||
|
||||
Here is a block diagram of how the major components fit together during a test run:
|
||||
Here is a block diagram of how the major components fit together during a test
|
||||
run:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
@@ -43,224 +58,469 @@ graph TD
|
||||
### Breakdown of Major Components
|
||||
|
||||
#### 1. The Playwright Test Runner
|
||||
The central brain of our testing environment. It handles parsing our TypeScript test specifications, managing test execution, starting the local web server, controlling the virtual browser instances, and generating reports.
|
||||
|
||||
The central brain of our testing environment. It handles parsing our TypeScript
|
||||
test specifications, managing test execution, starting the local web server,
|
||||
controlling the virtual browser instances, and generating reports.
|
||||
|
||||
#### 2. The Local Web Server (Vite / Preview)
|
||||
A local server hosting the Topola Viewer application. To ensure E2E tests validate the exact production bundle that will be deployed, the test runner in CI environments boots the production-built files via Vite's preview server (`npx vite preview --port 3000 --strictPort`), with the build process decoupled and executed earlier in the CI workflow job. Local environments dynamically run Vite dev server (`npx vite --no-open --port 3000 --strictPort`) or reuse an existing running server to maintain fast iteration cycles.
|
||||
|
||||
A local server hosting the Topola Viewer application. To ensure E2E tests
|
||||
validate the exact production bundle that will be deployed, the test runner in
|
||||
CI environments boots the production-built files via Vite's preview server
|
||||
(`npx vite preview --port 3000 --strictPort`), with the build process decoupled
|
||||
and executed earlier in the CI workflow job. Local environments dynamically run
|
||||
Vite dev server (`npx vite --no-open --port 3000 --strictPort`) or reuse an
|
||||
existing running server to maintain fast iteration cycles.
|
||||
|
||||
#### 3. The Built-in Network Router (Interception)
|
||||
The central network traffic controller. When the application attempts to fetch the family tree, our network router intercepts the request and returns our offline sample data instead. To prevent browser CORS blocks, the mock response explicitly supplies the `Access-Control-Allow-Origin: *` header. Wildcard interceptors match any request ending with `/family.ged`, allowing E2E tests to load family tree data consistently from local mock data without hardcoding public GitHub URL paths or relying on CORS proxy overrides.
|
||||
|
||||
The central network traffic controller. When the application attempts to fetch
|
||||
the family tree, our network router intercepts the request and returns our
|
||||
offline sample data instead. To prevent browser CORS blocks, the mock response
|
||||
explicitly supplies the `Access-Control-Allow-Origin: *` header. Wildcard
|
||||
interceptors match any request ending with `/family.ged`, allowing E2E tests to
|
||||
load family tree data consistently from local mock data without hardcoding
|
||||
public GitHub URL paths or relying on CORS proxy overrides.
|
||||
|
||||
#### 4. The Virtual Page Generator (Iframe Wrapper)
|
||||
A mechanism for generating temporary webpage wrappers directly in-memory during test execution. To simulate how our app works inside an iframe, Playwright serves a dynamic wrapper page on a mock URL `/test-embedded-frame.html`. This wrapper page loads our app inside an iframe and handles standard bidirectional postMessage handshakes. Because the application uses `HashRouter`, the iframe source points to the hash route `/#/view?embedded=true&handleCors=false` to ensure React Router matches the `/view` route.
|
||||
|
||||
A mechanism for generating temporary webpage wrappers directly in-memory during
|
||||
test execution. To simulate how our app works inside an iframe, Playwright
|
||||
serves a dynamic wrapper page on a mock URL `/test-embedded-frame.html`. This
|
||||
wrapper page loads our app inside an iframe and handles standard bidirectional
|
||||
postMessage handshakes. Because the application uses `HashRouter`, the iframe
|
||||
source points to the hash route `/#/view?embedded=true&handleCors=false` to
|
||||
ensure React Router matches the `/view` route.
|
||||
|
||||
#### 5. The Browser Environment Injector (Init Scripts)
|
||||
Allows the test runner to pre-configure the browser's environment before the web page's own code executes. This is essential for simulating experimental features like WebMCP tool registration. The injector exposes a mock registration API that pushes registered tools to the browser's global `window.__registeredTools` array. The runner evaluates execution blocks *inside* the browser context to invoke these tools' non-serializable callbacks, asserting tool registration and visual UI updates.
|
||||
|
||||
Allows the test runner to pre-configure the browser's environment before the web
|
||||
page's own code executes. This is essential for simulating experimental features
|
||||
like WebMCP tool registration. The injector exposes a mock registration API that
|
||||
pushes registered tools to the browser's global `window.__registeredTools`
|
||||
array. The runner evaluates execution blocks _inside_ the browser context to
|
||||
invoke these tools' non-serializable callbacks, asserting tool registration and
|
||||
visual UI updates.
|
||||
|
||||
## 3. Alternatives Considered & Rejected
|
||||
|
||||
To establish clear architectural guardrails and prevent future regressions or redundant work, this section documents the key design alternatives that were evaluated and subsequently ruled out.
|
||||
To establish clear architectural guardrails and prevent future regressions or
|
||||
redundant work, this section documents the key design alternatives that were
|
||||
evaluated and subsequently ruled out.
|
||||
|
||||
### A. Phased Migration (Coexistence)
|
||||
* **Considered:** Running both Cypress and Playwright concurrently in the repository and migrating the five test specs one by one over time.
|
||||
* **Rejected because:** Dual-framework coexistence introduces significant developer friction and technical debt. Developers would have to maintain duplicate configuration files, manage dual dependency pools in `package.json`, and configure complex dual execution stages in GitHub Actions. Given our E2E suite is compact, a "clean break" eliminates Cypress-related dependencies immediately.
|
||||
|
||||
- **Considered:** Running both Cypress and Playwright concurrently in the
|
||||
repository and migrating the five test specs one by one over time.
|
||||
- **Rejected because:** Dual-framework coexistence introduces significant
|
||||
developer friction and technical debt. Developers would have to maintain
|
||||
duplicate configuration files, manage dual dependency pools in `package.json`,
|
||||
and configure complex dual execution stages in GitHub Actions. Given our E2E
|
||||
suite is compact, a "clean break" eliminates Cypress-related dependencies
|
||||
immediately.
|
||||
|
||||
### B. Running E2E Tests Against Live URLs (Real-World Feeds)
|
||||
* **Considered:** Continuing the Cypress pattern of loading the sample family tree directly from GitHub raw servers over the public internet.
|
||||
* **Rejected because:** Live URL calls in automated E2E tests are a primary source of test flakiness. Tests can fail randomly due to DNS resolution lags, server downtime, or GitHub API rate-limiting, causing false negatives in our CI pipeline. Using Playwright's network routing to intercept these calls and fulfill them with a local fixture guarantees absolute hermeticity and consistency.
|
||||
|
||||
- **Considered:** Continuing the Cypress pattern of loading the sample family
|
||||
tree directly from GitHub raw servers over the public internet.
|
||||
- **Rejected because:** Live URL calls in automated E2E tests are a primary
|
||||
source of test flakiness. Tests can fail randomly due to DNS resolution lags,
|
||||
server downtime, or GitHub API rate-limiting, causing false negatives in our
|
||||
CI pipeline. Using Playwright's network routing to intercept these calls and
|
||||
fulfill them with a local fixture guarantees absolute hermeticity and
|
||||
consistency.
|
||||
|
||||
### C. Maintaining Physical HTML Wrapper Files for Iframe Tests
|
||||
* **Considered:** Storing a physical `tests/fixtures/embedded_frame.html` file in the repository to act as the container for iframe tests.
|
||||
* **Rejected because:** Storing a physical, standalone test-only wrapper file can create environment port synchronization issues and static asset mapping overhead.
|
||||
* **Actual Implementation Note:** The implementation adopted a hybrid approach. A physical template file [embedded_frame.html](../tests/fixtures/embedded_frame.html) is maintained as the structural source of truth for the frame, but it is loaded in-memory and served virtually on `/test-embedded-frame.html` via the network router, keeping it on the same origin/port dynamically to bypass cross-origin iframe blocks.
|
||||
|
||||
- **Considered:** Storing a physical `tests/fixtures/embedded_frame.html` file
|
||||
in the repository to act as the container for iframe tests.
|
||||
- **Rejected because:** Storing a physical, standalone test-only wrapper file
|
||||
can create environment port synchronization issues and static asset mapping
|
||||
overhead.
|
||||
- **Actual Implementation Note:** The implementation adopted a hybrid approach.
|
||||
A physical template file
|
||||
[embedded_frame.html](../tests/fixtures/embedded_frame.html) is maintained as
|
||||
the structural source of truth for the frame, but it is loaded in-memory and
|
||||
served virtually on `/test-embedded-frame.html` via the network router,
|
||||
keeping it on the same origin/port dynamically to bypass cross-origin iframe
|
||||
blocks.
|
||||
|
||||
### D. Retaining `start-server-and-test` for Dev Server Bootstrapping
|
||||
* **Considered:** Continuing to rely on `start-server-and-test` or a custom bash script to verify when port `3000` is responsive before running tests.
|
||||
* **Rejected because:** Playwright's native `webServer` orchestrator is superior, highly optimized, and self-contained. It handles port polling, processes recycling on failures, and performs automatic process cleanups upon exit natively. Keeping `start-server-and-test` adds unnecessary external library dependencies and script complexity.
|
||||
|
||||
- **Considered:** Continuing to rely on `start-server-and-test` or a custom bash
|
||||
script to verify when port `3000` is responsive before running tests.
|
||||
- **Rejected because:** Playwright's native `webServer` orchestrator is
|
||||
superior, highly optimized, and self-contained. It handles port polling,
|
||||
processes recycling on failures, and performs automatic process cleanups upon
|
||||
exit natively. Keeping `start-server-and-test` adds unnecessary external
|
||||
library dependencies and script complexity.
|
||||
|
||||
### E. Initial Multi-Browser & Multi-Device Targets
|
||||
* **Considered:** Configuring Chromium, Firefox, WebKit, and mobile emulation from Day One.
|
||||
* **Rejected because:** The immediate goal is to achieve full, stable parity with the existing single-browser Cypress suite. Adding multiple engines right away increases execution time and CI resource consumption. Multi-browser testing can be easily enabled later by adjusting the configuration in `playwright.config.ts`.
|
||||
|
||||
- **Considered:** Configuring Chromium, Firefox, WebKit, and mobile emulation
|
||||
from Day One.
|
||||
- **Rejected because:** The immediate goal is to achieve full, stable parity
|
||||
with the existing single-browser Cypress suite. Adding multiple engines right
|
||||
away increases execution time and CI resource consumption. Multi-browser
|
||||
testing can be easily enabled later by adjusting the configuration in
|
||||
`playwright.config.ts`.
|
||||
|
||||
## 4. Detailed Implementation Plan
|
||||
|
||||
This section defines the granular step-by-step instructions and enumerates **every single file** that will be created, modified, or deleted to ensure a flawless migration, including complete, copy-pasteable code contents for all configurations and test specifications matching the actual implementation.
|
||||
This section defines the granular step-by-step instructions and enumerates
|
||||
**every single file** that will be created, modified, or deleted to ensure a
|
||||
flawless migration, including complete, copy-pasteable code contents for all
|
||||
configurations and test specifications matching the actual implementation.
|
||||
|
||||
### A. Enumeration of Files
|
||||
|
||||
#### 1. Files to [DELETE]
|
||||
|
||||
* **[cypress.config.ts](../cypress.config.ts)**
|
||||
* *Rationale:* Complete removal of Cypress configurations; no longer needed.
|
||||
* **[cypress/e2e/chart_view.cy.js](../cypress/e2e/chart_view.cy.js)**
|
||||
* *Rationale:* Outdated JavaScript test spec. Replaced by type-safe `tests/chart_view.spec.ts`.
|
||||
* **[cypress/e2e/embedded.cy.js](../cypress/e2e/embedded.cy.js)**
|
||||
* *Rationale:* Outdated JavaScript test spec. Replaced by type-safe `tests/embedded.spec.ts`.
|
||||
* **[cypress/e2e/intro.cy.js](../cypress/e2e/intro.cy.js)**
|
||||
* *Rationale:* Outdated JavaScript test spec. Replaced by type-safe `tests/intro.spec.ts`.
|
||||
* **[cypress/e2e/search.cy.js](../cypress/e2e/search.cy.js)**
|
||||
* *Rationale:* Outdated JavaScript test spec. Replaced by type-safe `tests/search.spec.ts`.
|
||||
* **[cypress/e2e/webmcp.cy.js](../cypress/e2e/webmcp.cy.js)**
|
||||
* *Rationale:* Outdated JavaScript test spec. Replaced by type-safe `tests/webmcp.spec.ts`.
|
||||
* **`cypress/e2e/README.md`**
|
||||
* *Rationale:* Legacy test documentation that is no longer applicable.
|
||||
- **[cypress.config.ts](../cypress.config.ts)**
|
||||
- _Rationale:_ Complete removal of Cypress configurations; no longer needed.
|
||||
- **[cypress/e2e/chart_view.cy.js](../cypress/e2e/chart_view.cy.js)**
|
||||
- _Rationale:_ Outdated JavaScript test spec. Replaced by type-safe
|
||||
`tests/chart_view.spec.ts`.
|
||||
- **[cypress/e2e/embedded.cy.js](../cypress/e2e/embedded.cy.js)**
|
||||
- _Rationale:_ Outdated JavaScript test spec. Replaced by type-safe
|
||||
`tests/embedded.spec.ts`.
|
||||
- **[cypress/e2e/intro.cy.js](../cypress/e2e/intro.cy.js)**
|
||||
- _Rationale:_ Outdated JavaScript test spec. Replaced by type-safe
|
||||
`tests/intro.spec.ts`.
|
||||
- **[cypress/e2e/search.cy.js](../cypress/e2e/search.cy.js)**
|
||||
- _Rationale:_ Outdated JavaScript test spec. Replaced by type-safe
|
||||
`tests/search.spec.ts`.
|
||||
- **[cypress/e2e/webmcp.cy.js](../cypress/e2e/webmcp.cy.js)**
|
||||
- _Rationale:_ Outdated JavaScript test spec. Replaced by type-safe
|
||||
`tests/webmcp.spec.ts`.
|
||||
- **`cypress/e2e/README.md`**
|
||||
- _Rationale:_ Legacy test documentation that is no longer applicable.
|
||||
|
||||
#### 2. Files to [MODIFY]
|
||||
|
||||
* **[package.json](../package.json)**
|
||||
* *Rationale:* Uninstall `cypress` and `start-server-and-test` devDependencies. Install `@playwright/test` and `@types/node`. Add the silent `"preview": "vite preview"` script, replace the `cy:*` test scripts with Playwright E2E execution targets (`"test:e2e": "playwright test"` and `"test:e2e:ui": "playwright test --ui"`), and update the `"prettier"` and `"lint"` scripts to format and lint both the `src/` and `tests/` directories.
|
||||
* **[jest.config.ts](../jest.config.ts)**
|
||||
* *Rationale:* Add the `roots` configuration property to isolate Jest unit testing to the `src/` directory, preventing Jest from scanning or executing Playwright specs inside the `tests/` folder.
|
||||
* **[.gitignore](../.gitignore)**
|
||||
* *Rationale:* Ignore locally generated Playwright E2E testing artifacts (`playwright-report/`, `test-results/`, and `.playwright/`) to maintain a clean working tree.
|
||||
* **[PROJECT_STRUCTURE.md](../PROJECT_STRUCTURE.md)**
|
||||
* *Rationale:* Replace references to Cypress (`cypress/`, `cypress.config.ts`) with Playwright (`tests/`, `playwright.config.ts`) to keep the repository structural documentation fully accurate.
|
||||
* **[.github/workflows/node.js.yml](../.github/workflows/node.js.yml)**
|
||||
* *Rationale:* Replace `npm run cy:start-and-run` with cached Playwright execution. E2E tests are executed across all matrix Node environments to maximize testing coverage and parity across runtimes.
|
||||
* **[.github/workflows/deploy-gh-pages.yml](../.github/workflows/deploy-gh-pages.yml)**
|
||||
* *Rationale:* Remove testing and browser-setup steps entirely to speed up the deployment flow and separate deploy actions from regular test gating.
|
||||
* **[.github/workflows/deploy-wikitree-apps.yml](../.github/workflows/deploy-wikitree-apps.yml)**
|
||||
* *Rationale:* Remove testing and browser-setup steps entirely to speed up the deployment flow and separate deploy actions from regular test gating.
|
||||
- **[package.json](../package.json)**
|
||||
- _Rationale:_ Uninstall `cypress` and `start-server-and-test`
|
||||
devDependencies. Install `@playwright/test` and `@types/node`. Add the
|
||||
silent `"preview": "vite preview"` script, replace the `cy:*` test scripts
|
||||
with Playwright E2E execution targets (`"test:e2e": "playwright test"` and
|
||||
`"test:e2e:ui": "playwright test --ui"`), and update the `"prettier"` and
|
||||
`"lint"` scripts to format and lint both the `src/` and `tests/`
|
||||
directories.
|
||||
- **[jest.config.ts](../jest.config.ts)**
|
||||
- _Rationale:_ Add the `roots` configuration property to isolate Jest unit
|
||||
testing to the `src/` directory, preventing Jest from scanning or executing
|
||||
Playwright specs inside the `tests/` folder.
|
||||
- **[.gitignore](../.gitignore)**
|
||||
- _Rationale:_ Ignore locally generated Playwright E2E testing artifacts
|
||||
(`playwright-report/`, `test-results/`, and `.playwright/`) to maintain a
|
||||
clean working tree.
|
||||
- **[PROJECT_STRUCTURE.md](../PROJECT_STRUCTURE.md)**
|
||||
- _Rationale:_ Replace references to Cypress (`cypress/`, `cypress.config.ts`)
|
||||
with Playwright (`tests/`, `playwright.config.ts`) to keep the repository
|
||||
structural documentation fully accurate.
|
||||
- **[.github/workflows/node.js.yml](../.github/workflows/node.js.yml)**
|
||||
- _Rationale:_ Replace `npm run cy:start-and-run` with cached Playwright
|
||||
execution. E2E tests are executed across all matrix Node environments to
|
||||
maximize testing coverage and parity across runtimes.
|
||||
- **[.github/workflows/deploy-gh-pages.yml](../.github/workflows/deploy-gh-pages.yml)**
|
||||
- _Rationale:_ Remove testing and browser-setup steps entirely to speed up the
|
||||
deployment flow and separate deploy actions from regular test gating.
|
||||
- **[.github/workflows/deploy-wikitree-apps.yml](../.github/workflows/deploy-wikitree-apps.yml)**
|
||||
- _Rationale:_ Remove testing and browser-setup steps entirely to speed up the
|
||||
deployment flow and separate deploy actions from regular test gating.
|
||||
|
||||
#### 3. Files to [NEW]
|
||||
|
||||
* **[playwright.config.ts](../playwright.config.ts)**
|
||||
* *Rationale:* Central configuration for Playwright. Configures parallel execution, defines a single Desktop Chrome (Chromium) project, sets the base URL and locale to `'en-US'`, and manages the local dev or preview servers dynamically on port 3000 with strict port enforcement.
|
||||
* **`tests/tsconfig.json`**
|
||||
* *Rationale:* Custom localized compiler configuration for E2E tests that extends the root `tsconfig.json` but includes NodeJS environment types explicitly and isolates global types from Jest, keeping E2E environments perfectly isolated. It sets `"noEmit": true` because E2E tests do not need to output compiled JS files.
|
||||
* **`tests/global.d.ts`**
|
||||
* *Rationale:* Custom global type declaration file for E2E tests to safely declare `__registeredTools` on the `Window` interface without TypeScript compiler warnings. Redundant overrides for `Navigator` are omitted because the test suite inherits it from the application's core WebMCP declarations.
|
||||
* **`tests/helpers.ts`**
|
||||
* *Rationale:* Shared test utilities to encapsulate wildcard route mocking for family tree fetching (`setupGedcomRoute`) and hermetic context routing (`setupHermeticEnvironment`). This avoids code duplication across spec files.
|
||||
* **`tests/fixtures/embedded_frame.html`**
|
||||
* *Rationale:* Physical template wrapper file defining the iframe and message-passing structure for embedded view E2E verification.
|
||||
* **`src/datasource/testdata/test.ged`**
|
||||
* *Rationale:* Instead of checking in a duplicate fixture file, we directly read the existing version-controlled test GEDCOM dataset located at `src/datasource/testdata/test.ged` inside the Playwright interceptors. This eliminates file duplication, reduces repository footprint, and ensures a single source of truth.
|
||||
* **`tests/intro.spec.ts`**
|
||||
* *Rationale:* Type-safe TS spec checking landing page layout, menu items, and basic static DOM presence.
|
||||
* **`tests/chart_view.spec.ts`**
|
||||
* *Rationale:* Type-safe TS spec checking interactive tree navigation, relying on Playwright's auto-waiting to settle D3 transitions, drawer details panels, and routing interception to block analytics/third-party APIs.
|
||||
* **`tests/search.spec.ts`**
|
||||
* *Rationale:* Type-safe TS spec checking the search autocompletion (using robust, user-facing locators like `page.getByPlaceholder('Search for people')` to target the input, and Playwright's auto-waiting to handle search debouncing).
|
||||
* **`tests/webmcp.spec.ts`**
|
||||
* *Rationale:* Type-safe TS spec verifying WebMCP tools. Emulates out-of-process tool executions inside `navigator.modelContext` by evaluating execution blocks within the browser context, using polling assertions to avoid React `useEffect` mount race conditions.
|
||||
* **`tests/embedded.spec.ts`**
|
||||
* *Rationale:* Type-safe TS spec verifying iframe embedded views using a virtually served template file `tests/fixtures/embedded_frame.html` executing the bidirectional postMessage handshake (`ready` / `gedcom`) matching production.
|
||||
- **[playwright.config.ts](../playwright.config.ts)**
|
||||
- _Rationale:_ Central configuration for Playwright. Configures parallel
|
||||
execution, defines a single Desktop Chrome (Chromium) project, sets the base
|
||||
URL and locale to `'en-US'`, and manages the local dev or preview servers
|
||||
dynamically on port 3000 with strict port enforcement.
|
||||
- **`tests/tsconfig.json`**
|
||||
- _Rationale:_ Custom localized compiler configuration for E2E tests that
|
||||
extends the root `tsconfig.json` but includes NodeJS environment types
|
||||
explicitly and isolates global types from Jest, keeping E2E environments
|
||||
perfectly isolated. It sets `"noEmit": true` because E2E tests do not need
|
||||
to output compiled JS files.
|
||||
- **`tests/global.d.ts`**
|
||||
- _Rationale:_ Custom global type declaration file for E2E tests to safely
|
||||
declare `__registeredTools` on the `Window` interface without TypeScript
|
||||
compiler warnings. Redundant overrides for `Navigator` are omitted because
|
||||
the test suite inherits it from the application's core WebMCP declarations.
|
||||
- **`tests/helpers.ts`**
|
||||
- _Rationale:_ Shared test utilities to encapsulate wildcard route mocking for
|
||||
family tree fetching (`setupGedcomRoute`) and hermetic context routing
|
||||
(`setupHermeticEnvironment`). This avoids code duplication across spec
|
||||
files.
|
||||
- **`tests/fixtures/embedded_frame.html`**
|
||||
- _Rationale:_ Physical template wrapper file defining the iframe and
|
||||
message-passing structure for embedded view E2E verification.
|
||||
- **`src/datasource/testdata/test.ged`**
|
||||
- _Rationale:_ Instead of checking in a duplicate fixture file, we directly
|
||||
read the existing version-controlled test GEDCOM dataset located at
|
||||
`src/datasource/testdata/test.ged` inside the Playwright interceptors. This
|
||||
eliminates file duplication, reduces repository footprint, and ensures a
|
||||
single source of truth.
|
||||
- **`tests/intro.spec.ts`**
|
||||
- _Rationale:_ Type-safe TS spec checking landing page layout, menu items, and
|
||||
basic static DOM presence.
|
||||
- **`tests/chart_view.spec.ts`**
|
||||
- _Rationale:_ Type-safe TS spec checking interactive tree navigation, relying
|
||||
on Playwright's auto-waiting to settle D3 transitions, drawer details
|
||||
panels, and routing interception to block analytics/third-party APIs.
|
||||
- **`tests/search.spec.ts`**
|
||||
- _Rationale:_ Type-safe TS spec checking the search autocompletion (using
|
||||
robust, user-facing locators like
|
||||
`page.getByPlaceholder('Search for people')` to target the input, and
|
||||
Playwright's auto-waiting to handle search debouncing).
|
||||
- **`tests/webmcp.spec.ts`**
|
||||
- _Rationale:_ Type-safe TS spec verifying WebMCP tools. Emulates
|
||||
out-of-process tool executions inside `navigator.modelContext` by evaluating
|
||||
execution blocks within the browser context, using polling assertions to
|
||||
avoid React `useEffect` mount race conditions.
|
||||
- **`tests/embedded.spec.ts`**
|
||||
- _Rationale:_ Type-safe TS spec verifying iframe embedded views using a
|
||||
virtually served template file `tests/fixtures/embedded_frame.html`
|
||||
executing the bidirectional postMessage handshake (`ready` / `gedcom`)
|
||||
matching production.
|
||||
|
||||
### B. Step-by-Step Execution Plan & Complete File Contents
|
||||
|
||||
#### Step 1: Dependency Purge & Clean Break
|
||||
|
||||
1. Remove legacy Cypress and server-tester modules from `package.json`:
|
||||
`npm uninstall cypress start-server-and-test`
|
||||
2. Delete the `cypress.config.ts` configuration and the recursive `cypress/` folder from disk.
|
||||
2. Delete the `cypress.config.ts` configuration and the recursive `cypress/`
|
||||
folder from disk.
|
||||
3. Install Playwright Test Framework and Node environment types:
|
||||
`npm install -D @playwright/test @types/node`
|
||||
4. Download the local browser binaries required for Playwright execution:
|
||||
`npx playwright install chromium`
|
||||
5. Update the `"prettier"` script in `package.json` to cover both `src/` and `tests/`:
|
||||
`"prettier": "prettier --write \"{src,tests}/**/*.{ts,tsx,json}\""`
|
||||
6. Modify `jest.config.ts` to isolate Jest testing to the `src/` directory and prevent test scanning collisions:
|
||||
5. Update the `"prettier"` script in `package.json` to cover both `src/` and
|
||||
`tests/`: `"prettier": "prettier --write \"{src,tests}/**/*.{ts,tsx,json}\""`
|
||||
6. Modify `jest.config.ts` to isolate Jest testing to the `src/` directory and
|
||||
prevent test scanning collisions:
|
||||
```typescript
|
||||
// Add this property to the exported config object in jest.config.ts
|
||||
roots: ["<rootDir>/src"],
|
||||
```
|
||||
7. Update the repository structural documentation [PROJECT_STRUCTURE.md](../PROJECT_STRUCTURE.md) to replace Cypress details with Playwright details.
|
||||
7. Update the repository structural documentation
|
||||
[PROJECT_STRUCTURE.md](../PROJECT_STRUCTURE.md) to replace Cypress details
|
||||
with Playwright details.
|
||||
|
||||
#### Step 2: Script Alignment & Silent Configs
|
||||
1. Update the `package.json` scripts:
|
||||
* Add `"preview": "vite preview"` to support serving the production build in CI.
|
||||
* Replace `cy:*` script targets with Playwright E2E commands:
|
||||
* `"test:e2e": "playwright test"`
|
||||
* `"test:e2e:ui": "playwright test --ui"`
|
||||
2. Author `playwright.config.ts` to orchestrate the `webServer` dynamically based on execution context, and poll port `3000`.
|
||||
|
||||
**Key Configuration Details for [playwright.config.ts](../playwright.config.ts):**
|
||||
* **Test Directory**: Target `./tests` folder.
|
||||
* **Parallelism & CI Tuning**: Enable fully parallel execution (`fullyParallel: true`), disable `forbidOnly` locally but enforce it in CI, and configure retries (2 in CI, 0 locally).
|
||||
* **Base Configuration**: Set the `baseURL` to `http://localhost:3000`, force the locale to `'en-US'` to ensure consistent translation keys across all runs, and capture traces on first retry.
|
||||
* **Browsers**: Set up a single project using standard Chromium devices (`Desktop Chrome`).
|
||||
* **Orchestrated WebServer**: Configure `webServer` to run the Vite dev server (`npx vite --no-open --port 3000 --strictPort`) locally or Vite preview (`npx vite preview --port 3000 --strictPort`) in CI, targeting port `3000` with strict port enforcement and reusing any existing local server instance only in local mode.
|
||||
1. Update the `package.json` scripts:
|
||||
- Add `"preview": "vite preview"` to support serving the production build in
|
||||
CI.
|
||||
- Replace `cy:*` script targets with Playwright E2E commands:
|
||||
- `"test:e2e": "playwright test"`
|
||||
- `"test:e2e:ui": "playwright test --ui"`
|
||||
2. Author `playwright.config.ts` to orchestrate the `webServer` dynamically
|
||||
based on execution context, and poll port `3000`.
|
||||
|
||||
**Key Configuration Details for
|
||||
[playwright.config.ts](../playwright.config.ts):**
|
||||
|
||||
- **Test Directory**: Target `./tests` folder.
|
||||
- **Parallelism & CI Tuning**: Enable fully parallel execution
|
||||
(`fullyParallel: true`), disable `forbidOnly` locally but enforce it in CI,
|
||||
and configure retries (2 in CI, 0 locally).
|
||||
- **Base Configuration**: Set the `baseURL` to `http://localhost:3000`, force
|
||||
the locale to `'en-US'` to ensure consistent translation keys across all runs,
|
||||
and capture traces on first retry.
|
||||
- **Browsers**: Set up a single project using standard Chromium devices
|
||||
(`Desktop Chrome`).
|
||||
- **Orchestrated WebServer**: Configure `webServer` to run the Vite dev server
|
||||
(`npx vite --no-open --port 3000 --strictPort`) locally or Vite preview
|
||||
(`npx vite preview --port 3000 --strictPort`) in CI, targeting port `3000`
|
||||
with strict port enforcement and reusing any existing local server instance
|
||||
only in local mode.
|
||||
|
||||
#### Step 3: Establish Test Directories & Compile Settings
|
||||
|
||||
1. Create the folder path `tests/` and its subfolder `tests/fixtures/`.
|
||||
2. Author `tests/tsconfig.json` to isolate test typings from the main application and Jest unit tests:
|
||||
* **Inheritance & Target**: Extends the root `tsconfig.json`, setting `module` and `moduleResolution` to `NodeNext` and targeting `ES2022`.
|
||||
* **Isolated Types**: Excludes Jest/app global typings and explicitly pulls in only the `"node"` types.
|
||||
* **No Output**: Sets `"noEmit": true` as E2E specs do not need compilation output.
|
||||
3. Author `tests/global.d.ts` to provide TypeScript type definitions for mocked window objects:
|
||||
* **Type Extension**: Declares `__registeredTools?` on the global `Window` interface to prevent TypeScript compilation errors during WebMCP mocks.
|
||||
4. Author `tests/helpers.ts` to provide reusable mock setups and routing interceptions:
|
||||
* **Hermetic Routing**: Implements a `setupHermeticEnvironment(context)` helper that intercepts tracking services (`**/*google-analytics.com/**`, `**/*googletagmanager.com/**`) and embeds baseline web fonts to guarantee deterministic and fast test execution.
|
||||
* **GEDCOM Mocks**: Implements a `setupGedcomRoute(context)` helper that reads the version-controlled local dataset (`src/datasource/testdata/test.ged`) and routes all requests matching `**/family.ged` to be fulfilled with it, serving a `200 OK` response with CORS enablement headers (`Access-Control-Allow-Origin: *`).
|
||||
5. Author the physical template wrapper file `tests/fixtures/embedded_frame.html` for testing embedded iframe communications:
|
||||
* **Structure**: Defines a standard wrapper document housing an iframe that points to the app's embedded route: `/#/view?embedded=true&handleCors=false`.
|
||||
* **Bidirectional Handshake**: Contains script block executing a `fetch()` to get the GEDCOM content. It listens for a `'ready'` postMessage from the child iframe, and posts the raw GEDCOM content back with a `'gedcom'` message once the handshake is complete.
|
||||
2. Author `tests/tsconfig.json` to isolate test typings from the main
|
||||
application and Jest unit tests:
|
||||
- **Inheritance & Target**: Extends the root `tsconfig.json`, setting
|
||||
`module` and `moduleResolution` to `NodeNext` and targeting `ES2022`.
|
||||
- **Isolated Types**: Excludes Jest/app global typings and explicitly pulls
|
||||
in only the `"node"` types.
|
||||
- **No Output**: Sets `"noEmit": true` as E2E specs do not need compilation
|
||||
output.
|
||||
3. Author `tests/global.d.ts` to provide TypeScript type definitions for mocked
|
||||
window objects:
|
||||
- **Type Extension**: Declares `__registeredTools?` on the global `Window`
|
||||
interface to prevent TypeScript compilation errors during WebMCP mocks.
|
||||
4. Author `tests/helpers.ts` to provide reusable mock setups and routing
|
||||
interceptions:
|
||||
- **Hermetic Routing**: Implements a `setupHermeticEnvironment(context)`
|
||||
helper that intercepts tracking services (`**/*google-analytics.com/**`,
|
||||
`**/*googletagmanager.com/**`) and embeds baseline web fonts to guarantee
|
||||
deterministic and fast test execution.
|
||||
- **GEDCOM Mocks**: Implements a `setupGedcomRoute(context)` helper that
|
||||
reads the version-controlled local dataset
|
||||
(`src/datasource/testdata/test.ged`) and routes all requests matching
|
||||
`**/family.ged` to be fulfilled with it, serving a `200 OK` response with
|
||||
CORS enablement headers (`Access-Control-Allow-Origin: *`).
|
||||
5. Author the physical template wrapper file
|
||||
`tests/fixtures/embedded_frame.html` for testing embedded iframe
|
||||
communications:
|
||||
- **Structure**: Defines a standard wrapper document housing an iframe that
|
||||
points to the app's embedded route:
|
||||
`/#/view?embedded=true&handleCors=false`.
|
||||
- **Bidirectional Handshake**: Contains script block executing a `fetch()` to
|
||||
get the GEDCOM content. It listens for a `'ready'` postMessage from the
|
||||
child iframe, and posts the raw GEDCOM content back with a `'gedcom'`
|
||||
message once the handshake is complete.
|
||||
|
||||
#### Step 4: Spec Translation & Spec Designs
|
||||
|
||||
##### 1. Intro Test (`tests/intro.spec.ts`)
|
||||
|
||||
Checks the landing page layout, menu items, and basic static DOM presence:
|
||||
* **Setup**: Leverages `beforeEach` to configure hermetic routes using the `setupHermeticEnvironment` helper, then loads the index page (`/`).
|
||||
* **Assertions**:
|
||||
* Verifies that the main intro landing text content (specifically checking for the presence of `'Examples'`) is visible on the page.
|
||||
* Asserts that core action buttons in the menu (exact text `'Open file'` and `'Load from URL'`) are properly rendered and visible to the user.
|
||||
|
||||
- **Setup**: Leverages `beforeEach` to configure hermetic routes using the
|
||||
`setupHermeticEnvironment` helper, then loads the index page (`/`).
|
||||
- **Assertions**:
|
||||
- Verifies that the main intro landing text content (specifically checking for
|
||||
the presence of `'Examples'`) is visible on the page.
|
||||
- Asserts that core action buttons in the menu (exact text `'Open file'` and
|
||||
`'Load from URL'`) are properly rendered and visible to the user.
|
||||
|
||||
##### 2. Chart View Test (`tests/chart_view.spec.ts`)
|
||||
Checks tree navigation, settles transitions, details panels, and routing interception:
|
||||
* **Setup**: Employs `beforeEach` to configure route intercepts via `setupGedcomRoute` and navigates to the app passing a mock URL parameter (`/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged`).
|
||||
* **Assertions**:
|
||||
* Verifies that the viewer loads and renders the chart nodes, asserting that the name `'Bonifacy'` is visible inside the `#content` container.
|
||||
* Tests chart interactive capabilities by clicking an individual node (e.g., `'Radobod'`) with `{force: true}` to navigate, asserting that a child node (e.g., `'Chike'`) animates and renders.
|
||||
* Verifies side-drawer details rendering by asserting that person-specific data (e.g., `'a random note'`) is visible.
|
||||
|
||||
Checks tree navigation, settles transitions, details panels, and routing
|
||||
interception:
|
||||
|
||||
- **Setup**: Employs `beforeEach` to configure route intercepts via
|
||||
`setupGedcomRoute` and navigates to the app passing a mock URL parameter
|
||||
(`/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged`).
|
||||
- **Assertions**:
|
||||
- Verifies that the viewer loads and renders the chart nodes, asserting that
|
||||
the name `'Bonifacy'` is visible inside the `#content` container.
|
||||
- Tests chart interactive capabilities by clicking an individual node (e.g.,
|
||||
`'Radobod'`) with `{force: true}` to navigate, asserting that a child node
|
||||
(e.g., `'Chike'`) animates and renders.
|
||||
- Verifies side-drawer details rendering by asserting that person-specific
|
||||
data (e.g., `'a random note'`) is visible.
|
||||
|
||||
##### 3. Search Test (`tests/search.spec.ts`)
|
||||
|
||||
Checks search input, suggestion popups, debouncing, and navigation updates:
|
||||
* **Setup**: Prepares GEDCOM routing via `setupGedcomRoute` and loads the chart view.
|
||||
* **Assertions**:
|
||||
* Targets the search input using the user-facing selector `page.getByPlaceholder('Search for people')`.
|
||||
* Fills in search queries (e.g., `'chik'`), verifies the autocomplete results panel (`.results`) debounces and renders the target suggestion (`'Chike'`).
|
||||
* Triggers selection by pressing `'Enter'` and asserts that the main chart view successfully shifts focus and renders the target person's node.
|
||||
|
||||
- **Setup**: Prepares GEDCOM routing via `setupGedcomRoute` and loads the chart
|
||||
view.
|
||||
- **Assertions**:
|
||||
- Targets the search input using the user-facing selector
|
||||
`page.getByPlaceholder('Search for people')`.
|
||||
- Fills in search queries (e.g., `'chik'`), verifies the autocomplete results
|
||||
panel (`.results`) debounces and renders the target suggestion (`'Chike'`).
|
||||
- Triggers selection by pressing `'Enter'` and asserts that the main chart
|
||||
view successfully shifts focus and renders the target person's node.
|
||||
|
||||
##### 4. WebMCP Test (`tests/webmcp.spec.ts`)
|
||||
Bridges out-of-process serialization, checks tool registrations, and asserts detail updates:
|
||||
* **Setup**: Configures GEDCOM intercepts. Prior to navigation, it injects an early script (`page.addInitScript`) that mocks `navigator.modelContext.registerTool` and `navigator.modelContext.unregisterTool` APIs, populating tool metadata in a global `window.__registeredTools` list.
|
||||
* **Assertions**:
|
||||
* Verifies all core WebMCP tools (`get_selected_person`, `search_indi`, `inspect_indi`, `focus_indi`, `find_relationship_path`, `get_ancestors`, `get_descendants`) register correctly. Uses a polling assertion (`page.waitForFunction`) to safely wait for the React hook mounting cycle.
|
||||
* Tests interactive integration by evaluating a script in the browser context that calls the registered `focus_indi` tool callback with target identifier (`{id: 'I21'}`), then asserts that the UI automatically updates the viewer focus to `'Chike'`.
|
||||
|
||||
Bridges out-of-process serialization, checks tool registrations, and asserts
|
||||
detail updates:
|
||||
|
||||
- **Setup**: Configures GEDCOM intercepts. Prior to navigation, it injects an
|
||||
early script (`page.addInitScript`) that mocks
|
||||
`navigator.modelContext.registerTool` and
|
||||
`navigator.modelContext.unregisterTool` APIs, populating tool metadata in a
|
||||
global `window.__registeredTools` list.
|
||||
- **Assertions**:
|
||||
- Verifies all core WebMCP tools (`get_selected_person`, `search_indi`,
|
||||
`inspect_indi`, `focus_indi`, `find_relationship_path`, `get_ancestors`,
|
||||
`get_descendants`) register correctly. Uses a polling assertion
|
||||
(`page.waitForFunction`) to safely wait for the React hook mounting cycle.
|
||||
- Tests interactive integration by evaluating a script in the browser context
|
||||
that calls the registered `focus_indi` tool callback with target identifier
|
||||
(`{id: 'I21'}`), then asserts that the UI automatically updates the viewer
|
||||
focus to `'Chike'`.
|
||||
|
||||
##### 5. Embedded Test (`tests/embedded.spec.ts`)
|
||||
Serves local wrapper page template virtually and handles complete bidirectional postMessage handshake:
|
||||
* **Setup**: Prepares standard GEDCOM routing. Reads the physical `embedded_frame.html` template from disk and registers a route interceptor serving it dynamically at `/test-embedded-frame.html` so both frames stay on the same origin.
|
||||
* **Assertions**:
|
||||
* Navigates to `/test-embedded-frame.html`.
|
||||
* Targets the inner iframe using `page.frameLocator('#topolaFrame')`.
|
||||
* Verifies that the bidirectional message passing functions correctly by asserting that the inner iframe successfully receives and renders the root family chart nodes (asserting the presence of `'Bonifacy'`).
|
||||
|
||||
Serves local wrapper page template virtually and handles complete bidirectional
|
||||
postMessage handshake:
|
||||
|
||||
- **Setup**: Prepares standard GEDCOM routing. Reads the physical
|
||||
`embedded_frame.html` template from disk and registers a route interceptor
|
||||
serving it dynamically at `/test-embedded-frame.html` so both frames stay on
|
||||
the same origin.
|
||||
- **Assertions**:
|
||||
- Navigates to `/test-embedded-frame.html`.
|
||||
- Targets the inner iframe using `page.frameLocator('#topolaFrame')`.
|
||||
- Verifies that the bidirectional message passing functions correctly by
|
||||
asserting that the inner iframe successfully receives and renders the root
|
||||
family chart nodes (asserting the presence of `'Bonifacy'`).
|
||||
|
||||
#### Step 5: CI Pipeline Alignment
|
||||
|
||||
Modify the GitHub Actions YAML scripts in `.github/workflows/` to replace all legacy Cypress execution steps. E2E testing runs on all Node environments in the pipeline matrix for comprehensive validation.
|
||||
Modify the GitHub Actions YAML scripts in `.github/workflows/` to replace all
|
||||
legacy Cypress execution steps. E2E testing runs on all Node environments in the
|
||||
pipeline matrix for comprehensive validation.
|
||||
|
||||
##### 1. Key Steps for `node.js.yml`
|
||||
* **Prettier & Lint Checks**: Run formatting checks across both the application and the new tests using `npx prettier --check "{src,tests}/**/*.{ts,tsx,json}"` and run `npm run lint`.
|
||||
* **Build & Test**: Run standard `npm run build` and unit tests via `npm test`.
|
||||
* **E2E Spec Typecheck**: Run `npx tsc -p tests/tsconfig.json --noEmit` to guarantee complete E2E spec compilation/type correctness.
|
||||
* **Playwright Browser Caching**: Extract the current Playwright version and cache the browser binaries (`~/.cache/ms-playwright`) dynamically using `actions/cache@v4` to prevent redundant downloads.
|
||||
* **Execution**: Install necessary OS libraries using `npx playwright install-deps chromium`, install the browser binary, and run the test runner with `npm run test:e2e`.
|
||||
* **Artifact Storage**: Archive the resulting `playwright-report/` directory on failure or completion using `actions/upload-artifact@v4` with a 30-day retention period.
|
||||
|
||||
- **Prettier & Lint Checks**: Run formatting checks across both the application
|
||||
and the new tests using
|
||||
`npx prettier --check "{src,tests}/**/*.{ts,tsx,json}"` and run
|
||||
`npm run lint`.
|
||||
- **Build & Test**: Run standard `npm run build` and unit tests via `npm test`.
|
||||
- **E2E Spec Typecheck**: Run `npx tsc -p tests/tsconfig.json --noEmit` to
|
||||
guarantee complete E2E spec compilation/type correctness.
|
||||
- **Playwright Browser Caching**: Extract the current Playwright version and
|
||||
cache the browser binaries (`~/.cache/ms-playwright`) dynamically using
|
||||
`actions/cache@v4` to prevent redundant downloads.
|
||||
- **Execution**: Install necessary OS libraries using
|
||||
`npx playwright install-deps chromium`, install the browser binary, and run
|
||||
the test runner with `npm run test:e2e`.
|
||||
- **Artifact Storage**: Archive the resulting `playwright-report/` directory on
|
||||
failure or completion using `actions/upload-artifact@v4` with a 30-day
|
||||
retention period.
|
||||
|
||||
##### 2. Key Steps for `deploy-gh-pages.yml` & `deploy-wikitree-apps.yml`
|
||||
* No testing steps are run within the deploy workflows. They are fully streamlined to execute checkouts, build production assets, and deploy immediately, leaving gating assertions to the primary merge-to-master CI loop (`node.js.yml`).
|
||||
|
||||
- No testing steps are run within the deploy workflows. They are fully
|
||||
streamlined to execute checkouts, build production assets, and deploy
|
||||
immediately, leaving gating assertions to the primary merge-to-master CI loop
|
||||
(`node.js.yml`).
|
||||
|
||||
## 5. Future Considerations
|
||||
|
||||
### A. Visual Regression (Screenshot) Testing
|
||||
While the initial migration focuses on full parity with standard Cypress text-assertion specs, Topola Viewer is a highly visual, SVG-driven charting engine. In a future iteration, we should introduce Playwright visual snapshot/regression testing using `expect(page.locator('#content svg')).toHaveScreenshot('default-chart.png')`. This will automatically catch layout regressions, overlaps, and styled box positioning issues that DOM text assertions cannot detect.
|
||||
|
||||
While the initial migration focuses on full parity with standard Cypress
|
||||
text-assertion specs, Topola Viewer is a highly visual, SVG-driven charting
|
||||
engine. In a future iteration, we should introduce Playwright visual
|
||||
snapshot/regression testing using
|
||||
`expect(page.locator('#content svg')).toHaveScreenshot('default-chart.png')`.
|
||||
This will automatically catch layout regressions, overlaps, and styled box
|
||||
positioning issues that DOM text assertions cannot detect.
|
||||
|
||||
### B. File Upload Flow Testing
|
||||
To ensure users can reliably upload their family trees locally, a future iteration should introduce E2E tests for local file ingestion:
|
||||
* **Single File Upload (.ged)**: Simulating selection and upload of `.ged` text files using Playwright's `setInputFiles()` or `filechooser` events, verifying that the application correctly transitions to the visual chart view.
|
||||
* **Multi-File / Image Upload**: Verifying that uploading a `.ged` file along with associated `.jpg` or `.png` image assets resolves and binds photos to individuals in the chart without errors.
|
||||
* **Fixture Management**: This will require checking in small, 1x1 pixel dummy images to act as local mock visual resources.
|
||||
|
||||
To ensure users can reliably upload their family trees locally, a future
|
||||
iteration should introduce E2E tests for local file ingestion:
|
||||
|
||||
- **Single File Upload (.ged)**: Simulating selection and upload of `.ged` text
|
||||
files using Playwright's `setInputFiles()` or `filechooser` events, verifying
|
||||
that the application correctly transitions to the visual chart view.
|
||||
- **Multi-File / Image Upload**: Verifying that uploading a `.ged` file along
|
||||
with associated `.jpg` or `.png` image assets resolves and binds photos to
|
||||
individuals in the chart without errors.
|
||||
- **Fixture Management**: This will require checking in small, 1x1 pixel dummy
|
||||
images to act as local mock visual resources.
|
||||
|
||||
### C. CI/CD Pipeline Optimization (Shared E2E Gating Job)
|
||||
Currently, the manual deployment workflow `deploy-everywhere.yml` triggers both `deploy-gh-pages.yml` and `deploy-wikitree-apps.yml` in parallel, which duplicates E2E test execution and results in running the slow Playwright test runner twice simultaneously. A future optimization should restructure the workflows to execute the E2E validation once as a gating pre-deploy job (`needs: e2e-validation`), or rely strictly on the main merge-to-master CI pipeline validation, ensuring manual deployments are fast, clean, and lightweight.
|
||||
|
||||
Currently, the manual deployment workflow `deploy-everywhere.yml` triggers both
|
||||
`deploy-gh-pages.yml` and `deploy-wikitree-apps.yml` in parallel, which
|
||||
duplicates E2E test execution and results in running the slow Playwright test
|
||||
runner twice simultaneously. A future optimization should restructure the
|
||||
workflows to execute the E2E validation once as a gating pre-deploy job
|
||||
(`needs: e2e-validation`), or rely strictly on the main merge-to-master CI
|
||||
pipeline validation, ensuring manual deployments are fast, clean, and
|
||||
lightweight.
|
||||
|
||||
@@ -3,34 +3,33 @@
|
||||
## 1. Problem Statement
|
||||
|
||||
Topola Viewer is deployed to two environments — GitHub Pages and
|
||||
apps.wikitree.com — and depends on external services outside of our control:
|
||||
the WikiTree API and a third-party CORS proxy (`topolaproxy.bieda.it`). Any of
|
||||
these moving parts can break independently of our code: the WikiTree API can
|
||||
change its response schema or rate-limit requests, the CORS proxy can go down
|
||||
or change its URL scheme, and a deployment can silently introduce a routing or
|
||||
build issue that only manifests in production. Our existing test suite is
|
||||
hermetic — it mocks all network calls — so it verifies code correctness but
|
||||
cannot detect when the live, deployed system stops working end-to-end. We need
|
||||
lightweight smoke tests ("probers") that run against the live deployed URLs to
|
||||
catch real-world breakage, both immediately after each deployment and on a
|
||||
daily schedule.
|
||||
apps.wikitree.com — and depends on external services outside of our control: the
|
||||
WikiTree API and a third-party CORS proxy (`topolaproxy.bieda.it`). Any of these
|
||||
moving parts can break independently of our code: the WikiTree API can change
|
||||
its response schema or rate-limit requests, the CORS proxy can go down or change
|
||||
its URL scheme, and a deployment can silently introduce a routing or build issue
|
||||
that only manifests in production. Our existing test suite is hermetic — it
|
||||
mocks all network calls — so it verifies code correctness but cannot detect when
|
||||
the live, deployed system stops working end-to-end. We need lightweight smoke
|
||||
tests ("probers") that run against the live deployed URLs to catch real-world
|
||||
breakage, both immediately after each deployment and on a daily schedule.
|
||||
|
||||
## 2. The Technical Plan
|
||||
|
||||
The prober system consists of four independent smoke tests, each targeting a
|
||||
specific combination of deployment target and data path. Three of the four
|
||||
tests launch a real browser and navigate to a live deployed URL. The fourth
|
||||
pulls the Docker image published to GHCR, runs it locally, and
|
||||
verifies that the containerized application starts and renders data. All tests
|
||||
verify that the chart renders, the side panel shows the expected person's name,
|
||||
and no error message is displayed.
|
||||
specific combination of deployment target and data path. Three of the four tests
|
||||
launch a real browser and navigate to a live deployed URL. The fourth pulls the
|
||||
Docker image published to GHCR, runs it locally, and verifies that the
|
||||
containerized application starts and renders data. All tests verify that the
|
||||
chart renders, the side panel shows the expected person's name, and no error
|
||||
message is displayed.
|
||||
|
||||
The four probers are:
|
||||
|
||||
1. **WikiTree direct API prober** — Loads a known WikiTree profile
|
||||
(`Skłodowska-2`) from the app deployed on `apps.wikitree.com`. This
|
||||
exercises the direct WikiTree API path (no CORS proxy) and confirms the
|
||||
WikiTree deployment is healthy.
|
||||
(`Skłodowska-2`) from the app deployed on `apps.wikitree.com`. This exercises
|
||||
the direct WikiTree API path (no CORS proxy) and confirms the WikiTree
|
||||
deployment is healthy.
|
||||
|
||||
2. **GitHub Pages GEDCOM prober** — Loads a GEDCOM file from a raw GitHub URL
|
||||
through the app on `pewu.github.io`. Because the app is not on the
|
||||
@@ -44,12 +43,12 @@ The four probers are:
|
||||
default. This confirms the CORS proxy is reachable from the WikiTree
|
||||
deployment.
|
||||
|
||||
4. **Docker container prober** — Pulls the Docker image published to GHCR
|
||||
by `deploy-docker.yml`, runs it locally with the test GEDCOM file mounted
|
||||
via `STATIC_URL`, and verifies that the application renders the chart.
|
||||
This exercises the Docker build path (multi-stage `Dockerfile`, Caddy
|
||||
server configuration, static URL template injection) and confirms the
|
||||
published container image starts and serves data correctly.
|
||||
4. **Docker container prober** — Pulls the Docker image published to GHCR by
|
||||
`deploy-docker.yml`, runs it locally with the test GEDCOM file mounted via
|
||||
`STATIC_URL`, and verifies that the application renders the chart. This
|
||||
exercises the Docker build path (multi-stage `Dockerfile`, Caddy server
|
||||
configuration, static URL template injection) and confirms the published
|
||||
container image starts and serves data correctly.
|
||||
|
||||
Each prober is a standalone GitHub Actions workflow that can be triggered in
|
||||
three ways: automatically after a deploy finishes, on a daily schedule, or
|
||||
@@ -110,21 +109,21 @@ flowchart TD
|
||||
|
||||
Each prober is a small Playwright test spec. Three specs run against live
|
||||
deployed URLs; the Docker prober spec runs against a local Docker container
|
||||
started by the workflow. The specs live in a separate `tests/probers/`
|
||||
directory with their own Playwright configuration
|
||||
(`playwright.prober.config.ts`) so they are completely isolated from the
|
||||
existing hermetic test suite. Note: because the existing `playwright.config.ts`
|
||||
uses `testDir: './tests'` and Playwright searches recursively, the e2e project
|
||||
in the existing config must add `testIgnore: ['*_visual.spec.ts', 'probers/**']`
|
||||
to prevent prober specs from being picked up by the regular CI test run
|
||||
(`npm run test:e2e` or `npm run test:visual`). The prober config does not start
|
||||
a local dev server — each spec navigates to a full absolute URL (or
|
||||
`localhost:8080` for the Docker prober). A successful prober means a user can
|
||||
load the app and see data; a failure means something in the chain is broken
|
||||
and triggers an email notification. Note: GitHub Actions only sends email
|
||||
notifications if the user has explicitly enabled email notifications in their
|
||||
GitHub notification settings (Settings → Notifications → Email). If email
|
||||
notifications are disabled, failures are only visible in the Actions UI.
|
||||
started by the workflow. The specs live in a separate `tests/probers/` directory
|
||||
with their own Playwright configuration (`playwright.prober.config.ts`) so they
|
||||
are completely isolated from the existing hermetic test suite. Note: because the
|
||||
existing `playwright.config.ts` uses `testDir: './tests'` and Playwright
|
||||
searches recursively, the e2e project in the existing config must add
|
||||
`testIgnore: ['*_visual.spec.ts', 'probers/**']` to prevent prober specs from
|
||||
being picked up by the regular CI test run (`npm run test:e2e` or
|
||||
`npm run test:visual`). The prober config does not start a local dev server —
|
||||
each spec navigates to a full absolute URL (or `localhost:8080` for the Docker
|
||||
prober). A successful prober means a user can load the app and see data; a
|
||||
failure means something in the chain is broken and triggers an email
|
||||
notification. Note: GitHub Actions only sends email notifications if the user
|
||||
has explicitly enabled email notifications in their GitHub notification settings
|
||||
(Settings → Notifications → Email). If email notifications are disabled,
|
||||
failures are only visible in the Actions UI.
|
||||
|
||||
## 3. Alternatives Considered & Rejected
|
||||
|
||||
@@ -133,143 +132,142 @@ explicitly rejected. They are documented here to prevent future re-litigation
|
||||
and to serve as guardrails against scope creep.
|
||||
|
||||
### Alternative A: Unit-level API integration tests against the live
|
||||
|
||||
WikiTree API
|
||||
|
||||
* **Considered:** Writing tests that call the raw `wikitree-js` library
|
||||
- **Considered:** Writing tests that call the raw `wikitree-js` library
|
||||
functions directly against the live WikiTree API, verifying response schemas
|
||||
and field presence.
|
||||
* **Why Rejected:** This tests the `wikitree-js` dependency, not our code. Our
|
||||
existing Jest unit tests already cover our transformation logic using
|
||||
mocked API responses. The goal of probers is to verify the full
|
||||
end-to-end chain — browser, deployed app, network, API, proxy — not to
|
||||
re-verify API response shapes. Adding a separate layer of API-level
|
||||
integration tests would duplicate coverage without catching deployment or
|
||||
proxy issues.
|
||||
- **Why Rejected:** This tests the `wikitree-js` dependency, not our code. Our
|
||||
existing Jest unit tests already cover our transformation logic using mocked
|
||||
API responses. The goal of probers is to verify the full end-to-end chain —
|
||||
browser, deployed app, network, API, proxy — not to re-verify API response
|
||||
shapes. Adding a separate layer of API-level integration tests would duplicate
|
||||
coverage without catching deployment or proxy issues.
|
||||
|
||||
### Alternative B: Testing the CORS proxy on apps.wikitree.com via the
|
||||
|
||||
WikiTree data path
|
||||
|
||||
* **Considered:** Forcing the WikiTree API calls through the CORS proxy when
|
||||
the app is deployed on `apps.wikitree.com`, to test the proxy from that
|
||||
- **Considered:** Forcing the WikiTree API calls through the CORS proxy when the
|
||||
app is deployed on `apps.wikitree.com`, to test the proxy from that domain.
|
||||
- **Why Rejected:** The app hardcodes `handleCors` based on hostname — on
|
||||
`apps.wikitree.com`, WikiTree API calls always go direct (no proxy). There is
|
||||
no URL parameter to override this for the WikiTree data source. Forcing the
|
||||
proxy path would require a code change for test-only purposes, which is not
|
||||
justified. Instead, the CORS proxy is tested on `apps.wikitree.com` through
|
||||
the GEDCOM-from-URL path, which uses the proxy by default regardless of
|
||||
domain.
|
||||
* **Why Rejected:** The app hardcodes `handleCors` based on hostname — on
|
||||
`apps.wikitree.com`, WikiTree API calls always go direct (no proxy). There
|
||||
is no URL parameter to override this for the WikiTree data source. Forcing
|
||||
the proxy path would require a code change for test-only purposes, which is
|
||||
not justified. Instead, the CORS proxy is tested on `apps.wikitree.com`
|
||||
through the GEDCOM-from-URL path, which uses the proxy by default
|
||||
regardless of domain.
|
||||
|
||||
### Alternative C: Monolithic prober workflow with multiple jobs
|
||||
|
||||
* **Considered:** A single `prober.yml` workflow containing all four prober
|
||||
- **Considered:** A single `prober.yml` workflow containing all four prober
|
||||
tests as separate jobs within it.
|
||||
* **Why Rejected:** Separate workflow files give finer-grained control in the
|
||||
- **Why Rejected:** Separate workflow files give finer-grained control in the
|
||||
GitHub Actions UI — each prober can be triggered, re-run, or inspected
|
||||
independently. They also allow each prober to declare a targeted `needs`
|
||||
dependency on only the relevant deploy job (e.g., the WikiTree prober
|
||||
depends on `deploy-wikitree-apps`, not `deploy-gh-pages`). A monolithic
|
||||
workflow would couple all probers to the same trigger and make partial
|
||||
failures harder to manage.
|
||||
dependency on only the relevant deploy job (e.g., the WikiTree prober depends
|
||||
on `deploy-wikitree-apps`, not `deploy-gh-pages`). A monolithic workflow would
|
||||
couple all probers to the same trigger and make partial failures harder to
|
||||
manage.
|
||||
|
||||
### Alternative D: Probers that depend on all deploys finishing
|
||||
|
||||
* **Considered:** Making all four probers wait for all of `deploy-gh-pages`,
|
||||
`deploy-wikitree-apps`, and `deploy-docker` to complete before running
|
||||
any of them.
|
||||
* **Why Rejected:** This unnecessarily delays probers whose target has
|
||||
already been deployed. The WikiTree probers only need the WikiTree deploy
|
||||
to finish; the GitHub Pages prober only needs the GitHub Pages deploy.
|
||||
Coupling them to all deploys adds latency without benefit, and means a
|
||||
failure in one deploy would block probers for the other.
|
||||
- **Considered:** Making all four probers wait for all of `deploy-gh-pages`,
|
||||
`deploy-wikitree-apps`, and `deploy-docker` to complete before running any of
|
||||
them.
|
||||
- **Why Rejected:** This unnecessarily delays probers whose target has already
|
||||
been deployed. The WikiTree probers only need the WikiTree deploy to finish;
|
||||
the GitHub Pages prober only needs the GitHub Pages deploy. Coupling them to
|
||||
all deploys adds latency without benefit, and means a failure in one deploy
|
||||
would block probers for the other.
|
||||
|
||||
### Alternative E: Unconditional sleep before every prober run
|
||||
|
||||
* **Considered:** Always waiting 3 minutes at the start of every prober run,
|
||||
- **Considered:** Always waiting 3 minutes at the start of every prober run,
|
||||
regardless of trigger source.
|
||||
* **Why Rejected:** The sleep is only necessary after a deploy, to allow
|
||||
GitHub Pages or WikiTree to propagate the new version. For daily scheduled
|
||||
runs and manual triggers, there is no recent deploy to wait for, so the
|
||||
sleep wastes 3 minutes. Instead, a `wait_for_propagation` input flag is
|
||||
passed as `true` only when the prober is invoked from the deploy workflow.
|
||||
- **Why Rejected:** The sleep is only necessary after a deploy, to allow GitHub
|
||||
Pages or WikiTree to propagate the new version. For daily scheduled runs and
|
||||
manual triggers, there is no recent deploy to wait for, so the sleep wastes 3
|
||||
minutes. Instead, a `wait_for_propagation` input flag is passed as `true` only
|
||||
when the prober is invoked from the deploy workflow.
|
||||
|
||||
### Alternative F: Correctness assertions against specific WikiTree profile
|
||||
|
||||
data
|
||||
|
||||
* **Considered:** Asserting detailed data fields (e.g., specific birth dates,
|
||||
parent IDs, spouse counts) from the `Skłodowska-2` WikiTree profile to
|
||||
verify data correctness.
|
||||
* **Why Rejected:** Probers are smoke tests — their job is to verify "does
|
||||
the pipe work?", not "is the data correct?". Data correctness is already
|
||||
verified by the hermetic test suite with controlled fixtures. Coupling
|
||||
probers to specific WikiTree profile data creates fragility: if anyone
|
||||
edits the WikiTree profile, the prober would break even though the system
|
||||
is healthy. Probers assert only that the expected person's name appears in
|
||||
the chart and side panel, and that no error is displayed.
|
||||
- **Considered:** Asserting detailed data fields (e.g., specific birth dates,
|
||||
parent IDs, spouse counts) from the `Skłodowska-2` WikiTree profile to verify
|
||||
data correctness.
|
||||
- **Why Rejected:** Probers are smoke tests — their job is to verify "does the
|
||||
pipe work?", not "is the data correct?". Data correctness is already verified
|
||||
by the hermetic test suite with controlled fixtures. Coupling probers to
|
||||
specific WikiTree profile data creates fragility: if anyone edits the WikiTree
|
||||
profile, the prober would break even though the system is healthy. Probers
|
||||
assert only that the expected person's name appears in the chart and side
|
||||
panel, and that no error is displayed.
|
||||
|
||||
## 4. Detailed Implementation Plan
|
||||
|
||||
This section enumerates every file that will be created or modified, in
|
||||
the order they should be implemented, along with the rationale for each
|
||||
change. The implementation is divided into five steps.
|
||||
This section enumerates every file that will be created or modified, in the
|
||||
order they should be implemented, along with the rationale for each change. The
|
||||
implementation is divided into five steps.
|
||||
|
||||
### Step 1: Prober Playwright configuration
|
||||
|
||||
**Create:** `playwright.prober.config.ts`
|
||||
|
||||
A separate Playwright configuration file dedicated to prober tests. This
|
||||
file is distinct from the existing `playwright.config.ts` and serves a
|
||||
different purpose: it does not start a local dev server, does not define
|
||||
visual regression projects, and runs only against live deployed URLs.
|
||||
A separate Playwright configuration file dedicated to prober tests. This file is
|
||||
distinct from the existing `playwright.config.ts` and serves a different
|
||||
purpose: it does not start a local dev server, does not define visual regression
|
||||
projects, and runs only against live deployed URLs.
|
||||
|
||||
Rationale for key configuration decisions:
|
||||
|
||||
* **No `webServer`** — The existing config starts a Vite dev/preview server
|
||||
on `localhost:3000`. The prober config does not use Playwright's
|
||||
`webServer` feature. Live-URL probers navigate to full absolute URLs;
|
||||
the Docker prober's workflow starts the container externally (via
|
||||
`docker run`) before the test runs, so Playwright connects to
|
||||
`localhost:8080` without a `webServer` definition.
|
||||
* **`testDir: './tests/probers'`** — Prober specs are isolated in their own
|
||||
directory. Additionally, the existing `playwright.config.ts` e2e project
|
||||
must add `testIgnore: ['*_visual.spec.ts', 'probers/**']` to prevent
|
||||
prober specs from being discovered by the regular CI test run, since
|
||||
Playwright searches `testDir` recursively.
|
||||
* **`fullyParallel: false`** — Tests run sequentially to avoid hammering the
|
||||
live WikiTree API and CORS proxy with concurrent requests, which could
|
||||
trigger rate-limiting.
|
||||
* **`retries: 2`** — The WikiTree API and CORS proxy can have transient
|
||||
- **No `webServer`** — The existing config starts a Vite dev/preview server on
|
||||
`localhost:3000`. The prober config does not use Playwright's `webServer`
|
||||
feature. Live-URL probers navigate to full absolute URLs; the Docker prober's
|
||||
workflow starts the container externally (via `docker run`) before the test
|
||||
runs, so Playwright connects to `localhost:8080` without a `webServer`
|
||||
definition.
|
||||
- **`testDir: './tests/probers'`** — Prober specs are isolated in their own
|
||||
directory. Additionally, the existing `playwright.config.ts` e2e project must
|
||||
add `testIgnore: ['*_visual.spec.ts', 'probers/**']` to prevent prober specs
|
||||
from being discovered by the regular CI test run, since Playwright searches
|
||||
`testDir` recursively.
|
||||
- **`fullyParallel: false`** — Tests run sequentially to avoid hammering the
|
||||
live WikiTree API and CORS proxy with concurrent requests, which could trigger
|
||||
rate-limiting.
|
||||
- **`retries: 2`** — The WikiTree API and CORS proxy can have transient
|
||||
failures. Two retries (same as the existing CI config) provides a buffer
|
||||
against flakiness without masking persistent failures.
|
||||
* **`timeout: 120000`** — The WikiTree API prober makes multiple sequential
|
||||
API calls (ancestors, descendants, relatives) that can take over 30
|
||||
seconds under load. The default 30s timeout is too short for live API
|
||||
probers; 120 seconds provides adequate headroom.
|
||||
* **`reporter: [['html', {open: 'never'}], ['list']]`** — Generates an HTML
|
||||
report for upload as a workflow artifact, plus list output for console
|
||||
logs. Without this, no HTML report is produced and there is nothing to
|
||||
upload.
|
||||
* **`forbidOnly: true`** — Since probers always run in CI, `forbidOnly`
|
||||
should be set to `true` to prevent `test.only` from accidentally blocking
|
||||
all other prober specs. (The existing config uses `forbidOnly:
|
||||
!!process.env.CI`, which achieves the same effect when `CI` is set, but
|
||||
probers should enforce this unconditionally.)
|
||||
* **Single project named `prober` using `devices['Desktop Chrome']`** — No
|
||||
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 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.
|
||||
* **No `expect.toHaveScreenshot`** — Probers do not do visual regression
|
||||
- **`timeout: 120000`** — The WikiTree API prober makes multiple sequential API
|
||||
calls (ancestors, descendants, relatives) that can take over 30 seconds under
|
||||
load. The default 30s timeout is too short for live API probers; 120 seconds
|
||||
provides adequate headroom.
|
||||
- **`reporter: [['html', {open: 'never'}], ['list']]`** — Generates an HTML
|
||||
report for upload as a workflow artifact, plus list output for console logs.
|
||||
Without this, no HTML report is produced and there is nothing to upload.
|
||||
- **`forbidOnly: true`** — Since probers always run in CI, `forbidOnly` should
|
||||
be set to `true` to prevent `test.only` from accidentally blocking all other
|
||||
prober specs. (The existing config uses `forbidOnly: !!process.env.CI`, which
|
||||
achieves the same effect when `CI` is set, but probers should enforce this
|
||||
unconditionally.)
|
||||
- **Single project named `prober` using `devices['Desktop Chrome']`** — No 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 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.
|
||||
- **No `expect.toHaveScreenshot`** — Probers do not do visual regression
|
||||
testing; that is handled by the existing visual test project.
|
||||
* **`trace: 'on-first-retry'`, `screenshot: 'only-on-failure'`,
|
||||
`video: 'on-first-retry'`** — For live-URL probers where failures are hard
|
||||
to reproduce, trace files, failure screenshots, and retry videos are
|
||||
essential for debugging.
|
||||
* **`locale: 'en-US'`** — Forces consistent rendering and translation keys,
|
||||
- **`trace: 'on-first-retry'`, `screenshot: 'only-on-failure'`,
|
||||
`video: 'on-first-retry'`** — For live-URL probers where failures are hard to
|
||||
reproduce, trace files, failure screenshots, and retry videos are essential
|
||||
for debugging.
|
||||
- **`locale: 'en-US'`** — Forces consistent rendering and translation keys,
|
||||
matching the existing CI config. Without this, the app renders in the CI
|
||||
runner's default locale, which is non-deterministic.
|
||||
|
||||
@@ -280,65 +278,65 @@ targets a different URL and asserts a different expected name.
|
||||
|
||||
**Create:** `tests/probers/wikitree.spec.ts`
|
||||
|
||||
* **Target URL:**
|
||||
- **Target URL:**
|
||||
`https://apps.wikitree.com/apps/wiech13/topola-viewer/#/view?source=wikitree&indi=Sk%C5%82odowska-2`
|
||||
(URL-encoded `Skłodowska-2` to avoid encoding ambiguity with the non-ASCII
|
||||
character `ł` in source code).
|
||||
* **Expected name:** `Skłodowska` (from the WikiTree profile
|
||||
`Skłodowska-2` — Marie Skłodowska-Curie). The chart displays
|
||||
`LastNameAtBirth`, which is `Skłodowska` for this profile.
|
||||
* **What it exercises:** WikiTree direct API (no CORS proxy), WikiTree
|
||||
- **Expected name:** `Skłodowska` (from the WikiTree profile `Skłodowska-2` —
|
||||
Marie Skłodowska-Curie). The chart displays `LastNameAtBirth`, which is
|
||||
`Skłodowska` for this profile.
|
||||
- **What it exercises:** WikiTree direct API (no CORS proxy), WikiTree
|
||||
deployment.
|
||||
* **Note:** Does not use `standalone=true` in the URL. The app defaults to
|
||||
- **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`).
|
||||
|
||||
**Create:** `tests/probers/gh-pages-gedcom.spec.ts`
|
||||
|
||||
* **Target URL:**
|
||||
- **Target URL:**
|
||||
`https://pewu.github.io/topola-viewer/#/view?url=https://raw.githubusercontent.com/PeWu/topola-viewer/master/src/datasource/testdata/test.ged&indi=I1`
|
||||
* **Expected name:** `Bonifacy` (individual `@I1@` in `test.ged`, line 16:
|
||||
- **Expected name:** `Bonifacy` (individual `@I1@` in `test.ged`, line 16:
|
||||
`1 NAME Bonifacy /Gibbs/`).
|
||||
* **What it exercises:** GitHub Pages deployment, CORS proxy
|
||||
(`topolaproxy.bieda.it`), GEDCOM-from-URL loading. The app uses the CORS
|
||||
proxy by default for GEDCOM URLs (`handleCors` defaults to `true` — see
|
||||
- **What it exercises:** GitHub Pages deployment, CORS proxy
|
||||
(`topolaproxy.bieda.it`), GEDCOM-from-URL loading. The app uses the CORS proxy
|
||||
by default for GEDCOM URLs (`handleCors` defaults to `true` — see
|
||||
`src/util/url_args.ts:156`).
|
||||
|
||||
**Create:** `tests/probers/wikitree-cors-gedcom.spec.ts`
|
||||
|
||||
* **Target URL:**
|
||||
- **Target 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`
|
||||
* **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`
|
||||
- **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`
|
||||
only affects WikiTree API calls, not GEDCOM URL fetches in
|
||||
`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.
|
||||
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`
|
||||
|
||||
* **Target URL:** `http://localhost:8080/` (local Docker container).
|
||||
* **Expected name:** `Bonifacy` (same GEDCOM test file, mounted into the
|
||||
- **Target URL:** `http://localhost:8080/` (local Docker container).
|
||||
- **Expected name:** `Bonifacy` (same GEDCOM test file, mounted into the
|
||||
container via `STATIC_URL=test.ged`).
|
||||
* **What it exercises:** Published Docker image from GHCR (multi-stage
|
||||
`Dockerfile` build output, Caddy server configuration, static URL
|
||||
template injection (`{{ env "STATIC_URL" }}` in `index.html`)), and app
|
||||
rendering with a pre-loaded GEDCOM.
|
||||
* **Note:** The workflow pulls the Docker image published to GHCR
|
||||
- **What it exercises:** Published Docker image from GHCR (multi-stage
|
||||
`Dockerfile` build output, Caddy server configuration, static URL template
|
||||
injection (`{{ env "STATIC_URL" }}` in `index.html`)), and app rendering with
|
||||
a pre-loaded GEDCOM.
|
||||
- **Note:** The workflow pulls the Docker image published to GHCR
|
||||
(`ghcr.io/pewu/topola-viewer:latest`), runs it with
|
||||
`docker run -p 8080:8080 -e STATIC_URL=test.ged`, mounts
|
||||
`src/datasource/testdata/test.ged` into the container, and points
|
||||
Playwright at `localhost:8080`. The app loads in non-standalone mode
|
||||
(because `staticUrl` is set) and navigates directly to the chart view
|
||||
(see `app.tsx` routing logic). The Docker image does not include Google
|
||||
credentials (`VITE_GOOGLE_CLIENT_ID`, `VITE_GOOGLE_API_KEY`), so the
|
||||
Google Drive integration is non-functional in the containerized app.
|
||||
This is acceptable for the prober, which only tests chart rendering.
|
||||
Note: the Docker prober tests the image published to GHCR by
|
||||
`deploy-docker.yml`, ensuring the published artifact is functional.
|
||||
`src/datasource/testdata/test.ged` into the container, and points Playwright
|
||||
at `localhost:8080`. The app loads in non-standalone mode (because `staticUrl`
|
||||
is set) and navigates directly to the chart view (see `app.tsx` routing
|
||||
logic). The Docker image does not include Google credentials
|
||||
(`VITE_GOOGLE_CLIENT_ID`, `VITE_GOOGLE_API_KEY`), so the Google Drive
|
||||
integration is non-functional in the containerized app. This is acceptable for
|
||||
the prober, which only tests chart rendering. Note: the Docker prober tests
|
||||
the image published to GHCR by `deploy-docker.yml`, ensuring the published
|
||||
artifact is functional.
|
||||
|
||||
**Shared test structure** (in `tests/probers/helpers.ts`, called by each spec):
|
||||
|
||||
@@ -368,40 +366,39 @@ Playwright's `getByTestId` searches the entire document, so it matches the
|
||||
|
||||
Selectors are derived from the source code:
|
||||
|
||||
* `#content` — main container, visible when chart state is `SHOWING_CHART`
|
||||
(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
|
||||
- `#content` — main container, visible when chart state is `SHOWING_CHART` (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>`
|
||||
component; the `error` class comes from the custom `className="error"`
|
||||
prop in `ErrorMessage`. The resulting DOM element is
|
||||
`<div class="ui negative message error">`, so the selector
|
||||
`.ui.error.message` matches it.
|
||||
* `.ui.errorPopup.message` — dismissable popup error (see
|
||||
- `.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>` component;
|
||||
the `error` class comes from the custom `className="error"` prop in
|
||||
`ErrorMessage`. The resulting DOM element is
|
||||
`<div class="ui negative message error">`, so the selector `.ui.error.message`
|
||||
matches it.
|
||||
- `.ui.errorPopup.message` — dismissable popup error (see
|
||||
`src/components/error_display.tsx`). As above, `ui` and `message` come from
|
||||
Semantic UI React's `<Message>`, and `errorPopup` comes from the
|
||||
custom `className="errorPopup"` prop. Note: `ErrorPopup` uses Semantic
|
||||
UI React's `<Portal>`, which renders its content at `document.body`
|
||||
level, not inside `#content` in the DOM. When the popup is closed
|
||||
(`open={false}`), the Portal renders nothing, so this assertion
|
||||
verifies absence rather than visibility.
|
||||
Semantic UI React's `<Message>`, and `errorPopup` comes from the custom
|
||||
`className="errorPopup"` prop. Note: `ErrorPopup` uses Semantic UI React's
|
||||
`<Portal>`, which renders its content at `document.body` level, not inside
|
||||
`#content` in the DOM. When the popup is closed (`open={false}`), the Portal
|
||||
renders nothing, so this assertion verifies absence rather than visibility.
|
||||
|
||||
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` returns `true` on non-mobile screens, so the `div.details`
|
||||
container is visible without any URL parameters.
|
||||
`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.
|
||||
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
|
||||
|
||||
@@ -413,16 +410,15 @@ permissions:
|
||||
actions: write
|
||||
```
|
||||
|
||||
All prober workflows should use `actions/checkout@v4` (not v2, which is
|
||||
used by some older deploy workflows).
|
||||
All prober workflows should use `actions/checkout@v4` (not v2, which is used by
|
||||
some older deploy workflows).
|
||||
|
||||
All prober workflows should set `timeout-minutes: 15` on each job to prevent
|
||||
hanging runs from consuming runner minutes (default GitHub Actions timeout is
|
||||
6 hours).
|
||||
hanging runs from consuming runner minutes (default GitHub Actions timeout is 6
|
||||
hours).
|
||||
|
||||
All prober workflows should define a `concurrency` group to prevent
|
||||
overlapping runs (e.g., a deploy-triggered run overlapping with a
|
||||
schedule-triggered run):
|
||||
All prober workflows should define a `concurrency` group to prevent overlapping
|
||||
runs (e.g., a deploy-triggered run overlapping with a schedule-triggered run):
|
||||
|
||||
```yaml
|
||||
concurrency:
|
||||
@@ -430,43 +426,41 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
```
|
||||
|
||||
`cancel-in-progress: false` ensures a deploy-triggered run is not cancelled
|
||||
by a scheduled run — both complete independently.
|
||||
`cancel-in-progress: false` ensures a deploy-triggered run is not cancelled by a
|
||||
scheduled run — both complete independently.
|
||||
|
||||
Four reusable workflow files, one per prober. The three live-URL probers
|
||||
are identical in structure — only the name and artifact name differ. The
|
||||
Docker prober has a different structure (it builds and runs the container
|
||||
before testing).
|
||||
Four reusable workflow files, one per prober. The three live-URL probers are
|
||||
identical in structure — only the name and artifact name differ. The Docker
|
||||
prober has a different structure (it builds and runs the container before
|
||||
testing).
|
||||
|
||||
**Create:** `.github/workflows/prober-wikitree.yml`
|
||||
|
||||
* **Triggers:** `workflow_call` (with `wait_for_propagation` input),
|
||||
`workflow_dispatch` (with `wait_for_propagation` input), `schedule`
|
||||
(daily at `0 5 * * *` UTC = ~6:00/7:00 CET).
|
||||
* **Depends on (when called from deploy):** `deploy-wikitree-apps.yml`
|
||||
only.
|
||||
* **Artifact name:** `prober-report-wikitree`.
|
||||
- **Triggers:** `workflow_call` (with `wait_for_propagation` input),
|
||||
`workflow_dispatch` (with `wait_for_propagation` input), `schedule` (daily at
|
||||
`0 5 * * *` UTC = ~6:00/7:00 CET).
|
||||
- **Depends on (when called from deploy):** `deploy-wikitree-apps.yml` only.
|
||||
- **Artifact name:** `prober-report-wikitree`.
|
||||
|
||||
**Create:** `.github/workflows/prober-gh-pages.yml`
|
||||
|
||||
* Same structure.
|
||||
* **Depends on (when called from deploy):** `deploy-gh-pages.yml` only.
|
||||
* **Artifact name:** `prober-report-gh-pages`.
|
||||
- Same structure.
|
||||
- **Depends on (when called from deploy):** `deploy-gh-pages.yml` only.
|
||||
- **Artifact name:** `prober-report-gh-pages`.
|
||||
|
||||
**Create:** `.github/workflows/prober-wikitree-cors.yml`
|
||||
|
||||
* Same structure.
|
||||
* **Depends on (when called from deploy):** `deploy-wikitree-apps.yml`
|
||||
only.
|
||||
* **Artifact name:** `prober-report-wikitree-cors`.
|
||||
- Same structure.
|
||||
- **Depends on (when called from deploy):** `deploy-wikitree-apps.yml` only.
|
||||
- **Artifact name:** `prober-report-wikitree-cors`.
|
||||
|
||||
**Create:** `.github/workflows/prober-docker.yml`
|
||||
|
||||
* **Triggers:** `workflow_call`, `workflow_dispatch`, `schedule`
|
||||
(daily at `0 5 * * *` UTC).
|
||||
* **Depends on (when called from deploy):** `deploy-docker.yml` only.
|
||||
* **Artifact name:** `prober-report-docker`.
|
||||
* **No `wait_for_propagation` input** — The Docker container is available
|
||||
- **Triggers:** `workflow_call`, `workflow_dispatch`, `schedule` (daily at
|
||||
`0 5 * * *` UTC).
|
||||
- **Depends on (when called from deploy):** `deploy-docker.yml` only.
|
||||
- **Artifact name:** `prober-report-docker`.
|
||||
- **No `wait_for_propagation` input** — The Docker container is available
|
||||
immediately after startup; no propagation delay is needed.
|
||||
|
||||
**Shared workflow structure** (live-URL probers):
|
||||
@@ -497,7 +491,7 @@ before testing).
|
||||
|
||||
**Docker prober workflow structure** (different from live-URL probers):
|
||||
|
||||
```
|
||||
````
|
||||
1. Checkout repository (actions/checkout@v4).
|
||||
2. Pull Docker image: docker pull ghcr.io/pewu/topola-viewer:latest
|
||||
(Pull the image published by deploy-docker.yml. This tests the actual
|
||||
@@ -527,27 +521,26 @@ before testing).
|
||||
sleep 1
|
||||
done
|
||||
curl -sf -o /dev/null http://localhost:8080/
|
||||
```
|
||||
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. 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).
|
||||
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.)
|
||||
```
|
||||
````
|
||||
|
||||
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. 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). 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.)
|
||||
|
||||
````
|
||||
|
||||
**`wait_for_propagation` input flag** (live-URL probers only):
|
||||
|
||||
@@ -580,7 +573,7 @@ jobs:
|
||||
deploy-docker:
|
||||
uses: ./.github/workflows/deploy-docker.yml
|
||||
secrets: inherit
|
||||
```
|
||||
````
|
||||
|
||||
After changes:
|
||||
|
||||
@@ -622,34 +615,34 @@ jobs:
|
||||
|
||||
Rationale for dependency mapping:
|
||||
|
||||
* `prober-wikitree` needs `deploy-wikitree-apps` — it tests the WikiTree
|
||||
- `prober-wikitree` needs `deploy-wikitree-apps` — it tests the WikiTree
|
||||
deployment.
|
||||
* `prober-gh-pages` needs `deploy-gh-pages` — it tests the GitHub Pages
|
||||
- `prober-gh-pages` needs `deploy-gh-pages` — it tests the GitHub Pages
|
||||
deployment.
|
||||
* `prober-wikitree-cors` needs `deploy-wikitree-apps` — it tests the
|
||||
WikiTree deployment (with CORS proxy).
|
||||
* `prober-docker` needs `deploy-docker` — it tests the Docker image
|
||||
published to GHCR by `deploy-docker.yml` (Dockerfile, Caddy config, app
|
||||
startup). It does not pass `wait_for_propagation` because the container is
|
||||
available immediately after `docker run`.
|
||||
* If a prober fails, the `deploy-everywhere` workflow is marked as failed
|
||||
(red X), triggering an email notification (if GitHub email notifications
|
||||
are enabled — see note in Section 2).
|
||||
* Note: Individual deploy workflows (`deploy-gh-pages.yml`,
|
||||
- `prober-wikitree-cors` needs `deploy-wikitree-apps` — it tests the WikiTree
|
||||
deployment (with CORS proxy).
|
||||
- `prober-docker` needs `deploy-docker` — it tests the Docker image published to
|
||||
GHCR by `deploy-docker.yml` (Dockerfile, Caddy config, app startup). It does
|
||||
not pass `wait_for_propagation` because the container is available immediately
|
||||
after `docker run`.
|
||||
- If a prober fails, the `deploy-everywhere` workflow is marked as failed (red
|
||||
X), triggering an email notification (if GitHub email notifications are
|
||||
enabled — see note in Section 2).
|
||||
- Note: Individual deploy workflows (`deploy-gh-pages.yml`,
|
||||
`deploy-wikitree-apps.yml`, `deploy-docker.yml`) also support
|
||||
`workflow_dispatch`. If a deploy is triggered directly (instead of
|
||||
through `deploy-everywhere.yml`), no probers run because probers are only
|
||||
called from `deploy-everywhere.yml`. To ensure probers always run after a
|
||||
deploy, always trigger deploys through `deploy-everywhere.yml`.
|
||||
`workflow_dispatch`. If a deploy is triggered directly (instead of through
|
||||
`deploy-everywhere.yml`), no probers run because probers are only called from
|
||||
`deploy-everywhere.yml`. To ensure probers always run after a deploy, always
|
||||
trigger deploys through `deploy-everywhere.yml`.
|
||||
|
||||
### Step 5: Update supporting files
|
||||
|
||||
**Modify:** `playwright.config.ts`
|
||||
|
||||
Add `testIgnore: ['*_visual.spec.ts', 'probers/**']` to the e2e project to
|
||||
prevent prober specs in `tests/probers/` from being discovered by the regular
|
||||
CI e2e test run. Without this, `npm run test:e2e` would try to execute
|
||||
prober specs against the local dev server, causing failures.
|
||||
prevent prober specs in `tests/probers/` from being discovered by the regular CI
|
||||
e2e test run. Without this, `npm run test:e2e` would try to execute prober specs
|
||||
against the local dev server, causing failures.
|
||||
|
||||
**Modify:** `package.json`
|
||||
|
||||
@@ -662,8 +655,7 @@ Add a `test:probers` script for running probers locally during development:
|
||||
**Modify:** `tests/tsconfig.json`
|
||||
|
||||
Add `probers/` to the `include` array so prober specs are type-checked by
|
||||
`tsc -p tests/tsconfig.json --noEmit` (which runs in CI via
|
||||
`node.js.yml`).
|
||||
`tsc -p tests/tsconfig.json --noEmit` (which runs in CI via `node.js.yml`).
|
||||
|
||||
Current state:
|
||||
|
||||
@@ -675,35 +667,34 @@ Current state:
|
||||
```
|
||||
|
||||
The existing `./**/*.ts` glob already includes `tests/probers/` — no
|
||||
modification needed. The `./**/*.d.ts` glob covers type declaration files
|
||||
and does not affect prober spec discovery.
|
||||
modification needed. The `./**/*.d.ts` glob covers type declaration files and
|
||||
does not affect prober spec discovery.
|
||||
|
||||
**Modify:** `.github/workflows/README.md`
|
||||
|
||||
Add entries for the four new prober workflows to the file registry, e.g.:
|
||||
|
||||
```markdown
|
||||
- [prober-wikitree.yml](prober-wikitree.yml): Reusable prober that
|
||||
smoke-tests the WikiTree direct API path on the live WikiTree deployment.
|
||||
Runs daily and after deploy.
|
||||
- [prober-gh-pages.yml](prober-gh-pages.yml): Reusable prober that
|
||||
smoke-tests the GitHub Pages deployment with GEDCOM-from-URL through the
|
||||
CORS proxy. Runs daily and after deploy.
|
||||
- [prober-wikitree-cors.yml](prober-wikitree-cors.yml): Reusable prober
|
||||
that smoke-tests the CORS proxy from the WikiTree deployment with
|
||||
GEDCOM-from-URL. Runs daily and after deploy.
|
||||
- [prober-docker.yml](prober-docker.yml): Reusable prober that
|
||||
smoke-tests the published Docker image from GHCR (Dockerfile, Caddy
|
||||
config, app startup) by pulling and running it locally. Runs daily and
|
||||
- [prober-wikitree.yml](prober-wikitree.yml): Reusable prober that smoke-tests
|
||||
the WikiTree direct API path on the live WikiTree deployment. Runs daily and
|
||||
after deploy.
|
||||
- [prober-gh-pages.yml](prober-gh-pages.yml): Reusable prober that smoke-tests
|
||||
the GitHub Pages deployment with GEDCOM-from-URL through the CORS proxy. Runs
|
||||
daily and after deploy.
|
||||
- [prober-wikitree-cors.yml](prober-wikitree-cors.yml): Reusable prober that
|
||||
smoke-tests the CORS proxy from the WikiTree deployment with GEDCOM-from-URL.
|
||||
Runs daily and after deploy.
|
||||
- [prober-docker.yml](prober-docker.yml): Reusable prober that smoke-tests the
|
||||
published Docker image from GHCR (Dockerfile, Caddy config, app startup) by
|
||||
pulling and running it locally. Runs daily and after deploy.
|
||||
```
|
||||
|
||||
**Create:** `tests/probers/README.md`
|
||||
|
||||
Document the prober test directory, explaining that these are live smoke
|
||||
tests (not hermetic), how to run them locally (`npm run test:probers`), and
|
||||
that they require network access to external services (WikiTree API, CORS
|
||||
proxy, GitHub raw URLs).
|
||||
Document the prober test directory, explaining that these are live smoke tests
|
||||
(not hermetic), how to run them locally (`npm run test:probers`), and that they
|
||||
require network access to external services (WikiTree API, CORS proxy, GitHub
|
||||
raw URLs).
|
||||
|
||||
**Modify:** `PROJECT_STRUCTURE.md`
|
||||
|
||||
@@ -715,60 +706,59 @@ Add entries for the new `tests/probers/` directory and
|
||||
Add an entry for this design document to the registry:
|
||||
|
||||
```markdown
|
||||
* **[PROBERS_DESIGN.md](PROBERS_DESIGN.md)**: Live prober smoke tests
|
||||
against deployed GitHub Pages, WikiTree URLs, and local Docker container,
|
||||
covering WikiTree API, CORS proxy, GEDCOM-from-URL, and Docker build paths.
|
||||
- **[PROBERS_DESIGN.md](PROBERS_DESIGN.md)**: Live prober smoke tests against
|
||||
deployed GitHub Pages, WikiTree URLs, and local Docker container, covering
|
||||
WikiTree API, CORS proxy, GEDCOM-from-URL, and Docker build paths.
|
||||
```
|
||||
|
||||
### Summary of all files
|
||||
|
||||
| File | Action | Purpose |
|
||||
|---|---|---|
|
||||
| `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 (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 |
|
||||
| `.github/workflows/prober-docker.yml` | Create | Reusable workflow: Docker prober (pulls GHCR image) |
|
||||
| `.github/workflows/deploy-everywhere.yml` | Modify | Add prober jobs with targeted deploy dependencies |
|
||||
| `tests/tsconfig.json` | Modify | Ensure prober specs are type-checked |
|
||||
| `tests/probers/README.md` | Create | Document prober directory and usage |
|
||||
| `.github/workflows/README.md` | Modify | Document new prober workflows |
|
||||
| `docs/README.md` | Modify | Add prober design doc to registry |
|
||||
| `PROJECT_STRUCTURE.md` | Modify | Add prober directory and config file |
|
||||
|
||||
| File | Action | Purpose |
|
||||
| -------------------------------------------- | ------ | ------------------------------------------------------------------- |
|
||||
| `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 (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 |
|
||||
| `.github/workflows/prober-docker.yml` | Create | Reusable workflow: Docker prober (pulls GHCR image) |
|
||||
| `.github/workflows/deploy-everywhere.yml` | Modify | Add prober jobs with targeted deploy dependencies |
|
||||
| `tests/tsconfig.json` | Modify | Ensure prober specs are type-checked |
|
||||
| `tests/probers/README.md` | Create | Document prober directory and usage |
|
||||
| `.github/workflows/README.md` | Modify | Document new prober workflows |
|
||||
| `docs/README.md` | Modify | Add prober design doc to registry |
|
||||
| `PROJECT_STRUCTURE.md` | Modify | Add prober directory and config file |
|
||||
|
||||
## 5. Future Considerations
|
||||
|
||||
### WikiTree Login Flow Prober
|
||||
|
||||
The current WikiTree prober tests the unauthenticated API path (loading a
|
||||
public profile without an authcode). A future prober could test the
|
||||
authenticated login flow — logging in with an authcode and verifying that
|
||||
private profiles are accessible. This would require obtaining a dedicated
|
||||
test account on wikitree.com and storing the authcode as a GitHub Actions
|
||||
secret. This is deferred because it adds complexity (secret management,
|
||||
authcode expiry, test account maintenance) and the unauthenticated path
|
||||
already covers the most common deployment scenario.
|
||||
The current WikiTree prober tests the unauthenticated API path (loading a public
|
||||
profile without an authcode). A future prober could test the authenticated login
|
||||
flow — logging in with an authcode and verifying that private profiles are
|
||||
accessible. This would require obtaining a dedicated test account on
|
||||
wikitree.com and storing the authcode as a GitHub Actions secret. This is
|
||||
deferred because it adds complexity (secret management, authcode expiry, test
|
||||
account maintenance) and the unauthenticated path already covers the most common
|
||||
deployment scenario.
|
||||
|
||||
### Google Drive Integration Prober
|
||||
|
||||
A prober for the Google Drive integration (loading a GEDCOM file from
|
||||
Google Drive) is not included. Google's OAuth flow is designed for human
|
||||
interaction and includes bot detection (CAPTCHA, device verification) that
|
||||
would likely prevent automated login. Additionally, the Google Drive
|
||||
integration requires `VITE_GOOGLE_CLIENT_ID` and `VITE_GOOGLE_API_KEY`
|
||||
secrets, which are not available in the prober environment. A possible
|
||||
workaround would be to use a pre-authorized service account or a long-lived
|
||||
refresh token stored as a secret, but this is complex and fragile. This is
|
||||
deferred until a reliable automation approach is identified.
|
||||
A prober for the Google Drive integration (loading a GEDCOM file from Google
|
||||
Drive) is not included. Google's OAuth flow is designed for human interaction
|
||||
and includes bot detection (CAPTCHA, device verification) that would likely
|
||||
prevent automated login. Additionally, the Google Drive integration requires
|
||||
`VITE_GOOGLE_CLIENT_ID` and `VITE_GOOGLE_API_KEY` secrets, which are not
|
||||
available in the prober environment. A possible workaround would be to use a
|
||||
pre-authorized service account or a long-lived refresh token stored as a secret,
|
||||
but this is complex and fragile. This is deferred until a reliable automation
|
||||
approach is identified.
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
# Topola Viewer Feature Designs
|
||||
|
||||
This directory contains technical design documents for some of the new features and architectural updates of the Topola Viewer.
|
||||
This directory contains technical design documents for some of the new features
|
||||
and architectural updates of the Topola Viewer.
|
||||
|
||||
These documents are inspired by the design methodology described in the blog post [Elephants, Goldfish and the New Golden Age of Software Engineering](https://drensin.medium.com/elephants-goldfish-and-the-new-golden-age-of-software-engineering-c33641a48874) by Dave Rensin.
|
||||
These documents are inspired by the design methodology described in the blog
|
||||
post
|
||||
[Elephants, Goldfish and the New Golden Age of Software Engineering](https://drensin.medium.com/elephants-goldfish-and-the-new-golden-age-of-software-engineering-c33641a48874)
|
||||
by Dave Rensin.
|
||||
|
||||
While new features *may* follow this methodology to ensure robust design validation and context safety before writing code, doing so is optional.
|
||||
While new features _may_ follow this methodology to ensure robust design
|
||||
validation and context safety before writing code, doing so is optional.
|
||||
|
||||
## Design Documents Registry
|
||||
|
||||
* **[DOCKER_DESIGN.md](DOCKER_DESIGN.md)**: Docker container packaging, lightweight Caddy web server configuration, and GitHub Actions publishing pipelines.
|
||||
* **[IMMEDIATE_FAMILY_SECTION_DESIGN.md](IMMEDIATE_FAMILY_SECTION_DESIGN.md)**: Side panel block consolidating parents, spouses, and children for efficient off-screen tree navigation.
|
||||
* **[PLAYWRIGHT_DESIGN.md](PLAYWRIGHT_DESIGN.md)**: Playwright E2E testing architecture, Vite development/preview server lifecycle integration, tracking blocker interceptors, and embedded iframe communication.
|
||||
* **[SCREENSHOT_TESTS_DESIGN.md](SCREENSHOT_TESTS_DESIGN.md)**: Pixel-perfect visual regression testing infrastructure, animation stabilization, sandbox environment/DOM sanitization, and isolated Playwright projects.
|
||||
* **[WEBMCP_DESIGN.md](WEBMCP_DESIGN.md)**: Model Context Protocol (MCP) bridge and TS tool registration for AI agent interaction.
|
||||
* **[PROBERS_DESIGN.md](PROBERS_DESIGN.md)**: Live prober smoke tests against deployed GitHub Pages, WikiTree URLs, and local Docker container, covering WikiTree API, CORS proxy, GEDCOM-from-URL, and Docker build paths.
|
||||
- **[DOCKER_DESIGN.md](DOCKER_DESIGN.md)**: Docker container packaging,
|
||||
lightweight Caddy web server configuration, and GitHub Actions publishing
|
||||
pipelines.
|
||||
- **[IMMEDIATE_FAMILY_SECTION_DESIGN.md](IMMEDIATE_FAMILY_SECTION_DESIGN.md)**:
|
||||
Side panel block consolidating parents, spouses, and children for efficient
|
||||
off-screen tree navigation.
|
||||
- **[PLAYWRIGHT_DESIGN.md](PLAYWRIGHT_DESIGN.md)**: Playwright E2E testing
|
||||
architecture, Vite development/preview server lifecycle integration, tracking
|
||||
blocker interceptors, and embedded iframe communication.
|
||||
- **[SCREENSHOT_TESTS_DESIGN.md](SCREENSHOT_TESTS_DESIGN.md)**: Pixel-perfect
|
||||
visual regression testing infrastructure, animation stabilization, sandbox
|
||||
environment/DOM sanitization, and isolated Playwright projects.
|
||||
- **[WEBMCP_DESIGN.md](WEBMCP_DESIGN.md)**: Model Context Protocol (MCP) bridge
|
||||
and TS tool registration for AI agent interaction.
|
||||
- **[PROBERS_DESIGN.md](PROBERS_DESIGN.md)**: Live prober smoke tests against
|
||||
deployed GitHub Pages, WikiTree URLs, and local Docker container, covering
|
||||
WikiTree API, CORS proxy, GEDCOM-from-URL, and Docker build paths.
|
||||
|
||||
@@ -2,142 +2,308 @@
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
Topola Viewer is a highly interactive, visual genealogy exploration tool that renders family trees using complex SVG layouts and D3 configurations. As the codebase evolves, minor updates to CSS styles, React components, or underlying layout algorithms can easily introduce subtle visual regressions—such as overlapping text labels, misaligned parent-child connector lines, or broken formatting in the side panels—that standard text-based DOM tests cannot detect. To prevent these visual bugs from reaching production, we are introducing an automated screenshot (visual regression) testing suite using Playwright. This testing suite will capture pixel-perfect snapshots of critical interface states, automatically flag unintended visual changes, and guarantee a consistently polished, premium user experience across all releases.
|
||||
Topola Viewer is a highly interactive, visual genealogy exploration tool that
|
||||
renders family trees using complex SVG layouts and D3 configurations. As the
|
||||
codebase evolves, minor updates to CSS styles, React components, or underlying
|
||||
layout algorithms can easily introduce subtle visual regressions—such as
|
||||
overlapping text labels, misaligned parent-child connector lines, or broken
|
||||
formatting in the side panels—that standard text-based DOM tests cannot detect.
|
||||
To prevent these visual bugs from reaching production, we are introducing an
|
||||
automated screenshot (visual regression) testing suite using Playwright. This
|
||||
testing suite will capture pixel-perfect snapshots of critical interface states,
|
||||
automatically flag unintended visual changes, and guarantee a consistently
|
||||
polished, premium user experience across all releases.
|
||||
|
||||
## 2. The Technical Plan
|
||||
|
||||
To consistently verify the user interface without introducing complex setups, the screenshot testing framework is built on a local-first, self-contained execution model. It operates by launching a virtual web browser, running the Topola Viewer application inside it, and checking it against stored master images (baselines).
|
||||
To consistently verify the user interface without introducing complex setups,
|
||||
the screenshot testing framework is built on a local-first, self-contained
|
||||
execution model. It operates by launching a virtual web browser, running the
|
||||
Topola Viewer application inside it, and checking it against stored master
|
||||
images (baselines).
|
||||
|
||||
This setup consists of four major parts working in harmony:
|
||||
|
||||
1. **The Test Orchestrator (Playwright):** This acts as the central manager. It starts our local web server, launches virtual browser instances, automates user actions (such as clicking buttons or navigation links), captures the screenshots, and does the pixel-by-pixel comparison against our baseline master images.
|
||||
2. **The Local Web Server:** A background web server hosting the Topola Viewer application code. It serves the frontend interface directly to the virtual browser so that the test context runs identically to our actual user deployments.
|
||||
3. **The Network Traffic Controller (Route Interceptor):** An in-memory network router managed by the orchestrator. When the browser attempts to download a genealogy file (e.g., `family.ged`) or load a person's photo, the router intercepts that request and immediately answers it with tiny, predefined test datasets (fixtures). This guarantees that the test runs completely offline, remains blazingly fast, and has absolute visual predictability.
|
||||
4. **The Environment Sanitizer:** A tiny automated script executed directly inside the browser window right before a screenshot is snapped. Its only job is to locate and overwrite dynamic or shifting text elements (like Git commit hashes or changelog dates) with fixed placeholders, ensuring they do not trigger false test failures.
|
||||
1. **The Test Orchestrator (Playwright):** This acts as the central manager. It
|
||||
starts our local web server, launches virtual browser instances, automates
|
||||
user actions (such as clicking buttons or navigation links), captures the
|
||||
screenshots, and does the pixel-by-pixel comparison against our baseline
|
||||
master images.
|
||||
2. **The Local Web Server:** A background web server hosting the Topola Viewer
|
||||
application code. It serves the frontend interface directly to the virtual
|
||||
browser so that the test context runs identically to our actual user
|
||||
deployments.
|
||||
3. **The Network Traffic Controller (Route Interceptor):** An in-memory network
|
||||
router managed by the orchestrator. When the browser attempts to download a
|
||||
genealogy file (e.g., `family.ged`) or load a person's photo, the router
|
||||
intercepts that request and immediately answers it with tiny, predefined test
|
||||
datasets (fixtures). This guarantees that the test runs completely offline,
|
||||
remains blazingly fast, and has absolute visual predictability.
|
||||
4. **The Environment Sanitizer:** A tiny automated script executed directly
|
||||
inside the browser window right before a screenshot is snapped. Its only job
|
||||
is to locate and overwrite dynamic or shifting text elements (like Git commit
|
||||
hashes or changelog dates) with fixed placeholders, ensuring they do not
|
||||
trigger false test failures.
|
||||
|
||||
## 3. Alternatives Considered & Rejected
|
||||
|
||||
To prevent future developer friction, avoid redundant debugging cycles, and establish firm design guardrails, the following technical alternatives were evaluated and rejected:
|
||||
To prevent future developer friction, avoid redundant debugging cycles, and
|
||||
establish firm design guardrails, the following technical alternatives were
|
||||
evaluated and rejected:
|
||||
|
||||
### Alternative A: Global Animation Freezing (`freeze=true` Query Parameter)
|
||||
* **Considered:** Forcing Topola's SVG engine to completely freeze all animations globally in E2E tests to prevent visual capturing mismatches.
|
||||
* **Why Rejected:** Topola Viewer's initial chart mounting is entirely static; D3 transitions are only triggered during interactive navigation (e.g., clicking to shift focus to a child node). Since the target snapshots capture the initial mount of a chart or isolated panel element, introducing a complex global animation freezing hook is redundant. Instead, utilizing Playwright's standard auto-waiting mechanism (which pauses until the SVG is fully loaded and stationary) provides flawless, stable captures naturally.
|
||||
|
||||
- **Considered:** Forcing Topola's SVG engine to completely freeze all
|
||||
animations globally in E2E tests to prevent visual capturing mismatches.
|
||||
- **Why Rejected:** Topola Viewer's initial chart mounting is entirely static;
|
||||
D3 transitions are only triggered during interactive navigation (e.g.,
|
||||
clicking to shift focus to a child node). Since the target snapshots capture
|
||||
the initial mount of a chart or isolated panel element, introducing a complex
|
||||
global animation freezing hook is redundant. Instead, utilizing Playwright's
|
||||
standard auto-waiting mechanism (which pauses until the SVG is fully loaded
|
||||
and stationary) provides flawless, stable captures naturally.
|
||||
|
||||
### Alternative B: Monolithic Reference GEDCOM File (`rich_details.ged`)
|
||||
* **Considered:** Maintaining a single, massive master `.ged` file containing a wide collection of custom individuals (with complex names, nested attributes, attached photos, and custom events) to serve all tests.
|
||||
* **Why Rejected:** Monolithic test fixtures introduce severe coupling and high maintenance overhead. If a developer tweaks a birth record to debug an event-layout test, it can unintentionally shift elements in unrelated parts of the tree, failing baselines for name-formatting or photo rendering. Instead, creating microscopic, ad-hoc GEDCOM strings inline within each test case guarantees complete visual isolation, makes test intents instantly readable, and speeds up parsing.
|
||||
|
||||
- **Considered:** Maintaining a single, massive master `.ged` file containing a
|
||||
wide collection of custom individuals (with complex names, nested attributes,
|
||||
attached photos, and custom events) to serve all tests.
|
||||
- **Why Rejected:** Monolithic test fixtures introduce severe coupling and high
|
||||
maintenance overhead. If a developer tweaks a birth record to debug an
|
||||
event-layout test, it can unintentionally shift elements in unrelated parts of
|
||||
the tree, failing baselines for name-formatting or photo rendering. Instead,
|
||||
creating microscopic, ad-hoc GEDCOM strings inline within each test case
|
||||
guarantees complete visual isolation, makes test intents instantly readable,
|
||||
and speeds up parsing.
|
||||
|
||||
### Alternative C: Build-Time Environment Variable Overrides (Git SHA/Time)
|
||||
* **Considered:** Overriding `VITE_GIT_TIME` and `VITE_GIT_SHA` at build time specifically for E2E testing.
|
||||
* **Why Rejected:** In production gating pipelines (such as GitHub Actions), the application is built and packaged into production-ready assets before the E2E job begins execution. Re-compiling Vite assets solely to inject static E2E values is slow, resource-intensive, and violates the rule of testing the exact binary that will be deployed. Instead, executing an in-browser DOM override (`page.evaluate`) right before screenshot execution is lightweight, self-contained, and requires zero alterations to the build flow or production bundle.
|
||||
|
||||
- **Considered:** Overriding `VITE_GIT_TIME` and `VITE_GIT_SHA` at build time
|
||||
specifically for E2E testing.
|
||||
- **Why Rejected:** In production gating pipelines (such as GitHub Actions), the
|
||||
application is built and packaged into production-ready assets before the E2E
|
||||
job begins execution. Re-compiling Vite assets solely to inject static E2E
|
||||
values is slow, resource-intensive, and violates the rule of testing the exact
|
||||
binary that will be deployed. Instead, executing an in-browser DOM override
|
||||
(`page.evaluate`) right before screenshot execution is lightweight,
|
||||
self-contained, and requires zero alterations to the build flow or production
|
||||
bundle.
|
||||
|
||||
### Alternative D: Strict Pixel-Perfect Matching (Zero-Tolerance)
|
||||
* **Considered:** Requiring absolute, 100% visual equivalence with zero pixel mismatch allowed.
|
||||
* **Why Rejected:** Slight discrepancies in font rendering, subpixel antialiasing, and color blending are unavoidable across different operating systems (macOS developers vs. Linux CI agents). Enforcing zero-tolerance leads to extremely brittle tests that fail constantly due to harmless system-level rendering differences. Instead, setting relaxed thresholds (`maxDiffPixelRatio: 0.05` and `threshold: 0.2`) filters out system noise while aggressively catching genuine layout bugs, overlapping elements, and formatting failures.
|
||||
|
||||
- **Considered:** Requiring absolute, 100% visual equivalence with zero pixel
|
||||
mismatch allowed.
|
||||
- **Why Rejected:** Slight discrepancies in font rendering, subpixel
|
||||
antialiasing, and color blending are unavoidable across different operating
|
||||
systems (macOS developers vs. Linux CI agents). Enforcing zero-tolerance leads
|
||||
to extremely brittle tests that fail constantly due to harmless system-level
|
||||
rendering differences. Instead, setting relaxed thresholds
|
||||
(`maxDiffPixelRatio: 0.05` and `threshold: 0.2`) filters out system noise
|
||||
while aggressively catching genuine layout bugs, overlapping elements, and
|
||||
formatting failures.
|
||||
|
||||
## 4. Detailed Implementation Plan
|
||||
|
||||
This section defines the granular, step-by-step implementation steps and enumerates every file that will be created or modified to complete this visual regression framework.
|
||||
This section defines the granular, step-by-step implementation steps and
|
||||
enumerates every file that will be created or modified to complete this visual
|
||||
regression framework.
|
||||
|
||||
### A. Enumeration of Files
|
||||
|
||||
#### 1. Files to [MODIFY]
|
||||
|
||||
* **[playwright.config.ts](../playwright.config.ts)**
|
||||
* *Rationale:* Isolate visual regression tests into a separate Playwright project (separate from standard functional E2E tests). This allows applying dedicated visual settings (like viewport locking, automatic scrollbar hiding, and custom screenshot mismatch thresholds) exclusively to visual tests without polluting standard E2E runs. Threshold settings are configured globally under `expect.toHaveScreenshot`.
|
||||
* **[package.json](../package.json)**
|
||||
* *Rationale:* Add dedicated npm script commands to target the standard E2E project (`--project=e2e`) and the isolated visual testing project (`--project=visual`), preventing slow screenshot tests from bloating standard developer verification cycles.
|
||||
- **[playwright.config.ts](../playwright.config.ts)**
|
||||
- _Rationale:_ Isolate visual regression tests into a separate Playwright
|
||||
project (separate from standard functional E2E tests). This allows applying
|
||||
dedicated visual settings (like viewport locking, automatic scrollbar
|
||||
hiding, and custom screenshot mismatch thresholds) exclusively to visual
|
||||
tests without polluting standard E2E runs. Threshold settings are configured
|
||||
globally under `expect.toHaveScreenshot`.
|
||||
- **[package.json](../package.json)**
|
||||
- _Rationale:_ Add dedicated npm script commands to target the standard E2E
|
||||
project (`--project=e2e`) and the isolated visual testing project
|
||||
(`--project=visual`), preventing slow screenshot tests from bloating
|
||||
standard developer verification cycles.
|
||||
|
||||
#### 2. Files to [NEW]
|
||||
|
||||
* **`tests/helpers.ts`**
|
||||
* *Rationale:* Provide shared E2E/visual testing helper utilities. Features `setupHermeticEnvironment()` to abort external tracking requests and embed local fonts (ensuring offline hermetic execution) and `setupGedcomRoute()` to serve a standard mock `.ged` dataset.
|
||||
* **`tests/intro_visual.spec.ts`**
|
||||
* *Rationale:* Verify the landing page layout, copy block positions, and logo alignments. Employs an in-browser DOM script to overwrite dynamic footer versioning and dynamic changelog blocks prior to capture, ensuring baseline immunity.
|
||||
* **`tests/charts_visual.spec.ts`**
|
||||
* *Rationale:* Verify chart canvas boundaries, nodes, colors, and connections. Iterates over three of the supported layouts (`Hourglass`, `Relatives`, `Donatso`) using a simple tree, and captures screenshots of the stabilized D3 canvas.
|
||||
* **`tests/details_visual.spec.ts`**
|
||||
* *Rationale:* Verify details panel formats, image margins, fact headers, and sources. Defines tiny, ad-hoc mock GEDCOM inline strings for individual edge cases (long multi-part names, attached images, nested events) and serves pre-existing photo assets (e.g. `docker/examples/photos/photos/I1.jpg`) to render photos without broken image layouts.
|
||||
* **`tests/config_visual.spec.ts`**
|
||||
* *Rationale:* Verify the visual synchronization between Side Panel settings checkboxes/radio inputs and the SVG canvas. Captures full-viewport screenshots (at 1280x720) across the three curated configuration combinations.
|
||||
- **`tests/helpers.ts`**
|
||||
- _Rationale:_ Provide shared E2E/visual testing helper utilities. Features
|
||||
`setupHermeticEnvironment()` to abort external tracking requests and embed
|
||||
local fonts (ensuring offline hermetic execution) and `setupGedcomRoute()`
|
||||
to serve a standard mock `.ged` dataset.
|
||||
- **`tests/intro_visual.spec.ts`**
|
||||
- _Rationale:_ Verify the landing page layout, copy block positions, and logo
|
||||
alignments. Employs an in-browser DOM script to overwrite dynamic footer
|
||||
versioning and dynamic changelog blocks prior to capture, ensuring baseline
|
||||
immunity.
|
||||
- **`tests/charts_visual.spec.ts`**
|
||||
- _Rationale:_ Verify chart canvas boundaries, nodes, colors, and connections.
|
||||
Iterates over three of the supported layouts (`Hourglass`, `Relatives`,
|
||||
`Donatso`) using a simple tree, and captures screenshots of the stabilized
|
||||
D3 canvas.
|
||||
- **`tests/details_visual.spec.ts`**
|
||||
- _Rationale:_ Verify details panel formats, image margins, fact headers, and
|
||||
sources. Defines tiny, ad-hoc mock GEDCOM inline strings for individual edge
|
||||
cases (long multi-part names, attached images, nested events) and serves
|
||||
pre-existing photo assets (e.g. `docker/examples/photos/photos/I1.jpg`) to
|
||||
render photos without broken image layouts.
|
||||
- **`tests/config_visual.spec.ts`**
|
||||
- _Rationale:_ Verify the visual synchronization between Side Panel settings
|
||||
checkboxes/radio inputs and the SVG canvas. Captures full-viewport
|
||||
screenshots (at 1280x720) across the three curated configuration
|
||||
combinations.
|
||||
|
||||
### B. Step-by-Step Execution Plan
|
||||
|
||||
#### Step 1: Visual Project Isolation & Script Provisioning
|
||||
1. Open `playwright.config.ts` and configure separate projects within the projects array:
|
||||
* Define an `e2e` project using desktop Chrome settings that matches all `.spec.ts` files (`testMatch`) but excludes `*_visual.spec.ts` files (`testIgnore`).
|
||||
* Define a dedicated `visual` project that matches only `*_visual.spec.ts` files (`testMatch`), and locks the browser viewport to a width of `1280` and height of `720` pixels in the `use` configuration.
|
||||
2. Configure custom visual expectation thresholds globally under `expect.toHaveScreenshot` (specifically setting `maxDiffPixelRatio` to `0.05`, `threshold` to `0.2`, and `animations` to `'disabled'`).
|
||||
3. Open `package.json` and update the scripts to target standard and visual projects respectively:
|
||||
* `"test:e2e": "playwright test --project=e2e"` to run functional E2E tests exclusively.
|
||||
* `"test:visual": "playwright test --project=visual"` to run visual regression tests exclusively.
|
||||
* `"test:visual:update": "playwright test --project=visual --update-snapshots"` to automatically regenerate baseline reference files.
|
||||
|
||||
1. Open `playwright.config.ts` and configure separate projects within the
|
||||
projects array:
|
||||
- Define an `e2e` project using desktop Chrome settings that matches all
|
||||
`.spec.ts` files (`testMatch`) but excludes `*_visual.spec.ts` files
|
||||
(`testIgnore`).
|
||||
- Define a dedicated `visual` project that matches only `*_visual.spec.ts`
|
||||
files (`testMatch`), and locks the browser viewport to a width of `1280`
|
||||
and height of `720` pixels in the `use` configuration.
|
||||
2. Configure custom visual expectation thresholds globally under
|
||||
`expect.toHaveScreenshot` (specifically setting `maxDiffPixelRatio` to
|
||||
`0.05`, `threshold` to `0.2`, and `animations` to `'disabled'`).
|
||||
3. Open `package.json` and update the scripts to target standard and visual
|
||||
projects respectively:
|
||||
- `"test:e2e": "playwright test --project=e2e"` to run functional E2E tests
|
||||
exclusively.
|
||||
- `"test:visual": "playwright test --project=visual"` to run visual
|
||||
regression tests exclusively.
|
||||
- `"test:visual:update": "playwright test --project=visual --update-snapshots"`
|
||||
to automatically regenerate baseline reference files.
|
||||
|
||||
#### Step 2: Landing Page Visual Validation Spec (`tests/intro_visual.spec.ts`)
|
||||
1. Define a test block marked with the `@visual` tag, utilizing `setupHermeticEnvironment` helper in `beforeEach`.
|
||||
|
||||
1. Define a test block marked with the `@visual` tag, utilizing
|
||||
`setupHermeticEnvironment` helper in `beforeEach`.
|
||||
2. Instruct the browser to navigate to the root path `/`.
|
||||
3. Right before assertion, trigger `page.evaluate` to clean dynamic elements:
|
||||
* Target the `.version` class element and set `.innerText = "version: 2026-01-01 00:00 (testcommit)"`.
|
||||
* Target the changelog element (the container immediately following the "What's new" heading) and replace its HTML with a static placeholder change entry.
|
||||
- Target the `.version` class element and set
|
||||
`.innerText = "version: 2026-01-01 00:00 (testcommit)"`.
|
||||
- Target the changelog element (the container immediately following the
|
||||
"What's new" heading) and replace its HTML with a static placeholder change
|
||||
entry.
|
||||
4. Snap the screenshot using `expect(page).toHaveScreenshot('intro-page.png')`.
|
||||
|
||||
#### Step 3: Core SVG Canvas Layouts Spec (`tests/charts_visual.spec.ts`)
|
||||
1. Set up a `beforeEach` block that initializes `setupGedcomRoute(context)` from `helpers.ts` to intercept `**/family.ged` requests and fulfill them with raw GEDCOM test data.
|
||||
2. Write tests with the `@visual` tag iterating through the 3 supported layouts (`hourglass`, `relatives`, `donatso`):
|
||||
* Set browser route to `/#/view?url=https://example.org/family.ged&view=[hourglass|relatives|donatso]`.
|
||||
* Determine the appropriate container selector: `#dotatsoSvgContainer` if the view is `donatso`, otherwise `#svgContainer`.
|
||||
* Locate the container element, and call `locator.waitFor()` to ensure the element is fully attached and visible.
|
||||
* Wait for D3 rendering and layout stabilization using a brief layout-specific timeout (`waitTime`: `500ms` for hourglass/relatives, `1500ms` for donatso).
|
||||
* Capture the isolated canvas screenshot: `expect(container).toHaveScreenshot('chart-[type].png')`.
|
||||
|
||||
1. Set up a `beforeEach` block that initializes `setupGedcomRoute(context)` from
|
||||
`helpers.ts` to intercept `**/family.ged` requests and fulfill them with raw
|
||||
GEDCOM test data.
|
||||
2. Write tests with the `@visual` tag iterating through the 3 supported layouts
|
||||
(`hourglass`, `relatives`, `donatso`):
|
||||
- Set browser route to
|
||||
`/#/view?url=https://example.org/family.ged&view=[hourglass|relatives|donatso]`.
|
||||
- Determine the appropriate container selector: `#dotatsoSvgContainer` if the
|
||||
view is `donatso`, otherwise `#svgContainer`.
|
||||
- Locate the container element, and call `locator.waitFor()` to ensure the
|
||||
element is fully attached and visible.
|
||||
- Wait for D3 rendering and layout stabilization using a brief
|
||||
layout-specific timeout (`waitTime`: `500ms` for hourglass/relatives,
|
||||
`1500ms` for donatso).
|
||||
- Capture the isolated canvas screenshot:
|
||||
`expect(container).toHaveScreenshot('chart-[type].png')`.
|
||||
|
||||
#### Step 4: Details Panel Layouts Spec (`tests/details_visual.spec.ts`)
|
||||
1. Set up a `beforeEach` block to establish hermetic routes via `setupHermeticEnvironment(context)`.
|
||||
2. Define isolated test blocks with the `@visual` tag, each loading its own dedicated inline micro-GEDCOM dataset:
|
||||
* **Complex Names Test:**
|
||||
* Mock `**/family.ged` with a GEDCOM string containing prefix/suffix/rufname tags.
|
||||
* Navigate to the view route with `sidePanel=true`, locate the side panel container `#sidebar`.
|
||||
* Assert sidebar visual representation: `expect(page.locator('#sidebar')).toHaveScreenshot('details-complex-name.png')`.
|
||||
* **Image / Photo Rendering Test:**
|
||||
* Mock `**/family.ged` containing an `OBJE` tag pointing to a photo path (e.g. `photos/I1.jpg`).
|
||||
* Intercept requests for `**/photos/I1.jpg` and fulfill the request by serving the project asset `docker/examples/photos/photos/I1.jpg`.
|
||||
* Navigate, wait for the image load handler to complete (`img.waitFor({state: 'visible'})` and checking `image.complete` status).
|
||||
* Assert sidebar visual representation: `expect(page.locator('#sidebar')).toHaveScreenshot('details-photo-render.png')`.
|
||||
* **Custom Facts & Citations Test:**
|
||||
* Mock `**/family.ged` containing complex nested fact (`FACT`), source (`SOUR`), and note (`NOTE`) trees.
|
||||
* Select the individual and wait for `#sidebar` to load.
|
||||
* Assert sidebar visual representation: `expect(page.locator('#sidebar')).toHaveScreenshot('details-events-sources.png')`.
|
||||
* **Immediate Family Rendering Test:**
|
||||
* Mock `**/family.ged` containing an individual with explicit parental links (`FAMC`) and multi-partner spousal families (`FAMS`) to display biological parents, spouses, and chronologically sorted children blocks.
|
||||
* Select the individual and wait for `#sidebar` to load.
|
||||
* Assert sidebar visual representation: `expect(page.locator('#sidebar')).toHaveScreenshot('details-immediate-family.png')`.
|
||||
|
||||
1. Set up a `beforeEach` block to establish hermetic routes via
|
||||
`setupHermeticEnvironment(context)`.
|
||||
2. Define isolated test blocks with the `@visual` tag, each loading its own
|
||||
dedicated inline micro-GEDCOM dataset:
|
||||
- **Complex Names Test:**
|
||||
- Mock `**/family.ged` with a GEDCOM string containing
|
||||
prefix/suffix/rufname tags.
|
||||
- Navigate to the view route with `sidePanel=true`, locate the side panel
|
||||
container `#sidebar`.
|
||||
- Assert sidebar visual representation:
|
||||
`expect(page.locator('#sidebar')).toHaveScreenshot('details-complex-name.png')`.
|
||||
- **Image / Photo Rendering Test:**
|
||||
- Mock `**/family.ged` containing an `OBJE` tag pointing to a photo path
|
||||
(e.g. `photos/I1.jpg`).
|
||||
- Intercept requests for `**/photos/I1.jpg` and fulfill the request by
|
||||
serving the project asset `docker/examples/photos/photos/I1.jpg`.
|
||||
- Navigate, wait for the image load handler to complete
|
||||
(`img.waitFor({state: 'visible'})` and checking `image.complete` status).
|
||||
- Assert sidebar visual representation:
|
||||
`expect(page.locator('#sidebar')).toHaveScreenshot('details-photo-render.png')`.
|
||||
- **Custom Facts & Citations Test:**
|
||||
- Mock `**/family.ged` containing complex nested fact (`FACT`), source
|
||||
(`SOUR`), and note (`NOTE`) trees.
|
||||
- Select the individual and wait for `#sidebar` to load.
|
||||
- Assert sidebar visual representation:
|
||||
`expect(page.locator('#sidebar')).toHaveScreenshot('details-events-sources.png')`.
|
||||
- **Immediate Family Rendering Test:**
|
||||
- Mock `**/family.ged` containing an individual with explicit parental
|
||||
links (`FAMC`) and multi-partner spousal families (`FAMS`) to display
|
||||
biological parents, spouses, and chronologically sorted children blocks.
|
||||
- Select the individual and wait for `#sidebar` to load.
|
||||
- Assert sidebar visual representation:
|
||||
`expect(page.locator('#sidebar')).toHaveScreenshot('details-immediate-family.png')`.
|
||||
|
||||
#### Step 5: Configurations Integration Spec (`tests/config_visual.spec.ts`)
|
||||
1. Define a test block tagged `@visual` with a locked browser window viewport size of `1280x720` via `playwright.config.ts`.
|
||||
2. In `beforeEach`, mock `**/family.ged` using `setupGedcomRoute(context)`, load `/view?sidePanel=true`, wait for `#sidebar` and `#content` to be visible, and click the "Settings" tab (`await page.getByText('Settings', {exact: true}).click();`) to expose config fields.
|
||||
|
||||
1. Define a test block tagged `@visual` with a locked browser window viewport
|
||||
size of `1280x720` via `playwright.config.ts`.
|
||||
2. In `beforeEach`, mock `**/family.ged` using `setupGedcomRoute(context)`, load
|
||||
`/view?sidePanel=true`, wait for `#sidebar` and `#content` to be visible, and
|
||||
click the "Settings" tab
|
||||
(`await page.getByText('Settings', {exact: true}).click();`) to expose config
|
||||
fields.
|
||||
3. Assert the **Default Configuration (State 1)**:
|
||||
* Verify that both the checkbox states and the corresponding generation-colored SVG boxes are in alignment.
|
||||
* Assert the entire integrated screen: `expect(page).toHaveScreenshot('config-state-default.png')`.
|
||||
4. Automate panel clicks: Scope locators using `page.locator('form.details .item')` to target the "Colors" and "IDs" section items. Select the "by sex" color radio button and select the "hide" IDs option.
|
||||
5. Wait for updates (`page.waitForTimeout(300)`) and assert the **Sex Colors & No IDs Configuration (State 2)**:
|
||||
* Assert the entire integrated screen: `expect(page).toHaveScreenshot('config-state-gender-no-ids.png')`.
|
||||
6. Automate panel clicks: Scope locators using `page.locator('form.details .item')` to target the "Colors" and "Sex" section items. Select the "none" color radio button and select the "hide" sex option.
|
||||
7. Wait for updates (`page.waitForTimeout(300)`) and assert the **Minimalist Configuration (State 3)**:
|
||||
* Assert the entire integrated screen: `expect(page).toHaveScreenshot('config-state-minimalist.png')`.
|
||||
- Verify that both the checkbox states and the corresponding
|
||||
generation-colored SVG boxes are in alignment.
|
||||
- Assert the entire integrated screen:
|
||||
`expect(page).toHaveScreenshot('config-state-default.png')`.
|
||||
4. Automate panel clicks: Scope locators using
|
||||
`page.locator('form.details .item')` to target the "Colors" and "IDs" section
|
||||
items. Select the "by sex" color radio button and select the "hide" IDs
|
||||
option.
|
||||
5. Wait for updates (`page.waitForTimeout(300)`) and assert the **Sex Colors &
|
||||
No IDs Configuration (State 2)**:
|
||||
- Assert the entire integrated screen:
|
||||
`expect(page).toHaveScreenshot('config-state-gender-no-ids.png')`.
|
||||
6. Automate panel clicks: Scope locators using
|
||||
`page.locator('form.details .item')` to target the "Colors" and "Sex" section
|
||||
items. Select the "none" color radio button and select the "hide" sex option.
|
||||
7. Wait for updates (`page.waitForTimeout(300)`) and assert the **Minimalist
|
||||
Configuration (State 3)**:
|
||||
- Assert the entire integrated screen:
|
||||
`expect(page).toHaveScreenshot('config-state-minimalist.png')`.
|
||||
|
||||
## 5. CI/CD Pipeline Integration
|
||||
|
||||
To ensure that no visual regressions are introduced into the master branch, the visual testing suite is integrated into the GitHub Actions CI/CD workflow ([node.js.yml](../.github/workflows/node.js.yml)) alongside existing tests.
|
||||
To ensure that no visual regressions are introduced into the master branch, the
|
||||
visual testing suite is integrated into the GitHub Actions CI/CD workflow
|
||||
([node.js.yml](../.github/workflows/node.js.yml)) alongside existing tests.
|
||||
|
||||
### Pipeline Configuration
|
||||
Visual tests run sequentially after standard E2E tests. The workflow executes the following steps:
|
||||
1. **Install Dependencies:** Resolves Node.js package dependencies and installs/caches Playwright browser binaries (specifically `chromium`).
|
||||
|
||||
Visual tests run sequentially after standard E2E tests. The workflow executes
|
||||
the following steps:
|
||||
|
||||
1. **Install Dependencies:** Resolves Node.js package dependencies and
|
||||
installs/caches Playwright browser binaries (specifically `chromium`).
|
||||
2. **Run E2E Tests:** Runs standard functional tests using `npm run test:e2e`.
|
||||
3. **Run Visual Tests:** Runs visual regression tests using `npm run test:visual`.
|
||||
3. **Run Visual Tests:** Runs visual regression tests using
|
||||
`npm run test:visual`.
|
||||
|
||||
### Playwright HTML Reports in CI
|
||||
To prevent test reports from overwriting each other, the HTML reports for the different testing suites are output to distinct subdirectories within the `playwright-report` folder:
|
||||
* **E2E Tests:** Saved to `playwright-report/e2e` by setting the `PLAYWRIGHT_HTML_REPORT` environment variable.
|
||||
* **Visual Tests:** Saved to `playwright-report/visual` by setting the `PLAYWRIGHT_HTML_REPORT` environment variable.
|
||||
|
||||
Both reports are bundled and uploaded as a single workflow artifact (`playwright-report-${{ matrix.node-version }}`) on completion, allowing easy review of failures.
|
||||
To prevent test reports from overwriting each other, the HTML reports for the
|
||||
different testing suites are output to distinct subdirectories within the
|
||||
`playwright-report` folder:
|
||||
|
||||
- **E2E Tests:** Saved to `playwright-report/e2e` by setting the
|
||||
`PLAYWRIGHT_HTML_REPORT` environment variable.
|
||||
- **Visual Tests:** Saved to `playwright-report/visual` by setting the
|
||||
`PLAYWRIGHT_HTML_REPORT` environment variable.
|
||||
|
||||
Both reports are bundled and uploaded as a single workflow artifact
|
||||
(`playwright-report-${{ matrix.node-version }}`) on completion, allowing easy
|
||||
review of failures.
|
||||
|
||||
@@ -2,32 +2,102 @@
|
||||
|
||||
## Business Problem
|
||||
|
||||
When exploring large family trees in Topola Genealogy, users frequently need to locate specific individuals quickly using the search feature. Currently, accessing the search box requires moving the hand to the mouse or trackpad, navigating the cursor to the top bar, and clicking the input field, which breaks the flow of keyboard-driven navigation. This document proposes introducing a global keyboard shortcut (the `/` key) to instantly focus the search input, allowing users to search without manual mouse interaction. By streamlining this transition, the application provides a faster, more accessible, and premium keyboard-centric workflow for power users navigating complex genealogical data.
|
||||
When exploring large family trees in Topola Genealogy, users frequently need to
|
||||
locate specific individuals quickly using the search feature. Currently,
|
||||
accessing the search box requires moving the hand to the mouse or trackpad,
|
||||
navigating the cursor to the top bar, and clicking the input field, which breaks
|
||||
the flow of keyboard-driven navigation. This document proposes introducing a
|
||||
global keyboard shortcut (the `/` key) to instantly focus the search input,
|
||||
allowing users to search without manual mouse interaction. By streamlining this
|
||||
transition, the application provides a faster, more accessible, and premium
|
||||
keyboard-centric workflow for power users navigating complex genealogical data.
|
||||
|
||||
---
|
||||
|
||||
## Technical Plan
|
||||
|
||||
The shortcut mechanism is designed to be lightweight, modular, and resilient against responsive layout duplication. Rather than introducing complex third-party shortcut libraries, the feature leverages standard web browser event handling, custom React hooks, standard accessibility attributes, and a centralized input registry.
|
||||
The shortcut mechanism is designed to be lightweight, modular, and resilient
|
||||
against responsive layout duplication. Rather than introducing complex
|
||||
third-party shortcut libraries, the feature leverages standard web browser event
|
||||
handling, custom React hooks, standard accessibility attributes, and a
|
||||
centralized input registry.
|
||||
|
||||
### Major Components and Workflow
|
||||
|
||||
1. **The Encapsulated Hook (`useSearchShortcut`):** The keyboard shortcut logic is encapsulated within a custom hook `useSearchShortcut` (`src/menu/use_search_shortcut.ts`). Rather than being called by individual `SearchBar` components, the hook is invoked once at the layout level in `TopBar` (`src/menu/top_bar.tsx`). Individual `SearchBar` instances register their underlying `<input>` DOM elements in a centralized module-level registry (`registeredSearchInputs` `Set`). This ensures a single `window` `keydown` event listener manages focus across all responsive search bar instances.
|
||||
2. **The Modifier Guard:** To prevent conflicts with system-level and browser-level shortcuts (such as `Cmd + /` for help or extensions, or `Ctrl + /` for toggling comments), the listener ignores keydown events with `Cmd/Meta`. It also ignores `Ctrl` or `Alt` unless both are pressed simultaneously (`Ctrl + Alt`), allowing `AltGr` combinations on international keyboards to function properly.
|
||||
3. **The IME Composition & Default Prevented Guard:** To avoid hijacking keystrokes when international users are typing using an Input Method Editor (IME) for CJK (Chinese, Japanese, Korean) languages or when another event handler has already intercepted the keypress, the listener ignores keydown events when `event.isComposing === true` or `event.defaultPrevented === true`.
|
||||
4. **The Smart Filter (Collision, Repeat & Modal Guards):** Before taking action, the listener verifies `event.key === '/'` and checks three conditions:
|
||||
* **Collision Guard (`isTextEditable`):** It extracts the event path (supporting Shadow DOM encapsulation via `event.composedPath()`) and traverses all nodes in the path to check for `<input>`, `<textarea>`, `<select>`, `<button>`, elements with `contenteditable` active, or elements with `role="textbox"`, `role="searchbox"`, `role="spinbutton"`, or `role="combobox"`. This guarantees that typing `/` or pressing `/` while focused inside form controls, rich text editors, or ARIA fields does not trigger the shortcut.
|
||||
* **Repeat Guard:** It checks if the key event is repeated (`event.repeat === true`). If so, it ignores it to prevent key-repeat cycles.
|
||||
* **Modal Guard:** It inspects both the main `document` and the event target's `getRootNode()` for active modal dialogs (`dialog[open], .ui.modal.visible.active`) and ignores any modal with `aria-hidden="true"`. It verifies visibility (`checkVisibility() || getBoundingClientRect().width > 0`) without forcing synchronous layout recalculations (simple dropdown menus are not blocked). If an active visible modal is open, the shortcut is ignored to prevent stealing focus.
|
||||
If all guards pass and a visible search input is found, the listener intercepts the keypress (using `event.preventDefault()`) and initiates the focus transition.
|
||||
5. **The Centralized Search Input Registry:** Because `@artsy/fresnel` mounts both the desktop and mobile top bar menus in the DOM simultaneously, two separate instances of the `SearchBar` component exist concurrently.
|
||||
To avoid duplicate IDs in the DOM, `id="search"` is removed from the component. When mounted, each `SearchBar` registers its underlying HTML `<input>` element in a module-level `Set` (`registeredSearchInputs`) via `registerSearchInput` / `unregisterSearchInput`. When the shortcut is triggered, `useSearchShortcut` iterates through the registered inputs and identifies the active instance by checking if it is visible in the viewport (`input.checkVisibility() || input.getBoundingClientRect().width > 0`). Once found, it focuses and selects it:
|
||||
* Focus is requested with `{ preventScroll: true }` to avoid jarring page shifts.
|
||||
* `select()` is called on the HTML `<input>` element to highlight all existing text, allowing the user to immediately overwrite it with a new query.
|
||||
6. **The Translation Engine (Full Placeholder Localization):** To keep translation keys clean and avoid runtime string concatenation issues across different languages, two complete placeholder strings are defined: `"menu.search.placeholder"` (`"Search for people"`) and `"menu.search.placeholder_with_shortcut"` (`"Search for people (press '/')"`).
|
||||
* On desktop (where `hideShortcutHint` is false or omitted), `SearchBar` uses `placeholder_with_shortcut`.
|
||||
* On mobile (where `hideShortcutHint` is true), `SearchBar` uses `placeholder`, saving screen space on touch devices.
|
||||
7. **Accessibility (a11y):** The search input is decorated with the standard `aria-keyshortcuts="/"` attribute to declare the global shortcut to screen readers and assistive technologies.
|
||||
1. **The Encapsulated Hook (`useSearchShortcut`):** The keyboard shortcut logic
|
||||
is encapsulated within a custom hook `useSearchShortcut`
|
||||
(`src/menu/use_search_shortcut.ts`). Rather than being called by individual
|
||||
`SearchBar` components, the hook is invoked once at the layout level in
|
||||
`TopBar` (`src/menu/top_bar.tsx`). Individual `SearchBar` instances register
|
||||
their underlying `<input>` DOM elements in a centralized module-level
|
||||
registry (`registeredSearchInputs` `Set`). This ensures a single `window`
|
||||
`keydown` event listener manages focus across all responsive search bar
|
||||
instances.
|
||||
2. **The Modifier Guard:** To prevent conflicts with system-level and
|
||||
browser-level shortcuts (such as `Cmd + /` for help or extensions, or
|
||||
`Ctrl + /` for toggling comments), the listener ignores keydown events with
|
||||
`Cmd/Meta`. It also ignores `Ctrl` or `Alt` unless both are pressed
|
||||
simultaneously (`Ctrl + Alt`), allowing `AltGr` combinations on
|
||||
international keyboards to function properly.
|
||||
3. **The IME Composition & Default Prevented Guard:** To avoid hijacking
|
||||
keystrokes when international users are typing using an Input Method Editor
|
||||
(IME) for CJK (Chinese, Japanese, Korean) languages or when another event
|
||||
handler has already intercepted the keypress, the listener ignores keydown
|
||||
events when `event.isComposing === true` or
|
||||
`event.defaultPrevented === true`.
|
||||
4. **The Smart Filter (Collision, Repeat & Modal Guards):** Before taking
|
||||
action, the listener verifies `event.key === '/'` and checks three
|
||||
conditions:
|
||||
- **Collision Guard (`isTextEditable`):** It extracts the event path
|
||||
(supporting Shadow DOM encapsulation via `event.composedPath()`) and
|
||||
traverses all nodes in the path to check for `<input>`, `<textarea>`,
|
||||
`<select>`, `<button>`, elements with `contenteditable` active, or
|
||||
elements with `role="textbox"`, `role="searchbox"`, `role="spinbutton"`,
|
||||
or `role="combobox"`. This guarantees that typing `/` or pressing `/`
|
||||
while focused inside form controls, rich text editors, or ARIA fields does
|
||||
not trigger the shortcut.
|
||||
- **Repeat Guard:** It checks if the key event is repeated
|
||||
(`event.repeat === true`). If so, it ignores it to prevent key-repeat
|
||||
cycles.
|
||||
- **Modal Guard:** It inspects both the main `document` and the event
|
||||
target's `getRootNode()` for active modal dialogs
|
||||
(`dialog[open], .ui.modal.visible.active`) and ignores any modal with
|
||||
`aria-hidden="true"`. It verifies visibility
|
||||
(`checkVisibility() || getBoundingClientRect().width > 0`) without forcing
|
||||
synchronous layout recalculations (simple dropdown menus are not blocked).
|
||||
If an active visible modal is open, the shortcut is ignored to prevent
|
||||
stealing focus. If all guards pass and a visible search input is found,
|
||||
the listener intercepts the keypress (using `event.preventDefault()`) and
|
||||
initiates the focus transition.
|
||||
5. **The Centralized Search Input Registry:** Because `@artsy/fresnel` mounts
|
||||
both the desktop and mobile top bar menus in the DOM simultaneously, two
|
||||
separate instances of the `SearchBar` component exist concurrently. To avoid
|
||||
duplicate IDs in the DOM, `id="search"` is removed from the component. When
|
||||
mounted, each `SearchBar` registers its underlying HTML `<input>` element in
|
||||
a module-level `Set` (`registeredSearchInputs`) via `registerSearchInput` /
|
||||
`unregisterSearchInput`. When the shortcut is triggered, `useSearchShortcut`
|
||||
iterates through the registered inputs and identifies the active instance by
|
||||
checking if it is visible in the viewport
|
||||
(`input.checkVisibility() || input.getBoundingClientRect().width > 0`). Once
|
||||
found, it focuses and selects it:
|
||||
- Focus is requested with `{ preventScroll: true }` to avoid jarring page
|
||||
shifts.
|
||||
- `select()` is called on the HTML `<input>` element to highlight all
|
||||
existing text, allowing the user to immediately overwrite it with a new
|
||||
query.
|
||||
6. **The Translation Engine (Full Placeholder Localization):** To keep
|
||||
translation keys clean and avoid runtime string concatenation issues across
|
||||
different languages, two complete placeholder strings are defined:
|
||||
`"menu.search.placeholder"` (`"Search for people"`) and
|
||||
`"menu.search.placeholder_with_shortcut"`
|
||||
(`"Search for people (press '/')"`).
|
||||
- On desktop (where `hideShortcutHint` is false or omitted), `SearchBar`
|
||||
uses `placeholder_with_shortcut`.
|
||||
- On mobile (where `hideShortcutHint` is true), `SearchBar` uses
|
||||
`placeholder`, saving screen space on touch devices.
|
||||
7. **Accessibility (a11y):** The search input is decorated with the standard
|
||||
`aria-keyshortcuts="/"` attribute to declare the global shortcut to screen
|
||||
readers and assistive technologies.
|
||||
|
||||
### Component Interaction Diagram
|
||||
|
||||
@@ -50,12 +120,24 @@ graph TD
|
||||
## Alternatives Considered
|
||||
|
||||
### 1. Per-Component Event Listeners in `SearchBar`
|
||||
* **Alternative:** Invoke `useSearchShortcut` inside each `SearchBar` instance so that each component attaches its own `window` `keydown` event listener.
|
||||
* **Why Rejected:** Attaching multiple window event listeners concurrently (one for desktop, one for mobile) can lead to duplicate event handling or race conditions. Instead, `useSearchShortcut` is called once at the layout level in `TopBar`, while individual `SearchBar` components register their DOM `<input>` nodes in a shared module-level `Set` (`registeredSearchInputs`).
|
||||
|
||||
- **Alternative:** Invoke `useSearchShortcut` inside each `SearchBar` instance
|
||||
so that each component attaches its own `window` `keydown` event listener.
|
||||
- **Why Rejected:** Attaching multiple window event listeners concurrently (one
|
||||
for desktop, one for mobile) can lead to duplicate event handling or race
|
||||
conditions. Instead, `useSearchShortcut` is called once at the layout level in
|
||||
`TopBar`, while individual `SearchBar` components register their DOM `<input>`
|
||||
nodes in a shared module-level `Set` (`registeredSearchInputs`).
|
||||
|
||||
### 2. Using React Refs to Focus the Input
|
||||
* **Alternative:** Pass a React `ref` to `<Search>` and call `focus()` or `getBoundingClientRect()` directly on it.
|
||||
* **Why Rejected:** Semantic UI React's `Search` / `Input` components are class components or wrappers where refs do not consistently return the raw HTML `<input>` DOM node required for calling `select()`. Querying the wrapper element (`wrapperRef.current.querySelector('input')`) reliably targets the underlying HTML input element for registration.
|
||||
|
||||
- **Alternative:** Pass a React `ref` to `<Search>` and call `focus()` or
|
||||
`getBoundingClientRect()` directly on it.
|
||||
- **Why Rejected:** Semantic UI React's `Search` / `Input` components are class
|
||||
components or wrappers where refs do not consistently return the raw HTML
|
||||
`<input>` DOM node required for calling `select()`. Querying the wrapper
|
||||
element (`wrapperRef.current.querySelector('input')`) reliably targets the
|
||||
underlying HTML input element for registration.
|
||||
|
||||
---
|
||||
|
||||
@@ -64,34 +146,63 @@ graph TD
|
||||
### Affected Files and Rationales
|
||||
|
||||
#### 1. [use_search_shortcut.ts](file:///home/pwiech/personal/github/topola-viewer/src/menu/use_search_shortcut.ts)
|
||||
* **Rationale:** New custom hook and registry file. Implements `registerSearchInput` / `unregisterSearchInput` to manage mounted search input instances in a module-level `Set`. Implements `isTextEditable(event)` to guard against input hijacking across standard form controls (`<input>`, `<textarea>`, `<select>`, `<button>`) and ARIA text fields (`textbox`, `searchbox`, `spinbutton`, `combobox`) by traversing all nodes in the composed path. Implements `isModalActive(event)` to verify active visible modals (`dialog[open], .ui.modal.visible.active`) across main document and Shadow DOM roots (ignoring `aria-hidden="true"`). Implements `useSearchShortcut()` to attach a single window `keydown` listener (matching `event.key === '/'` with `AltGr` support), iterate registered inputs to identify the visible instance, intercept events with `preventDefault()`, and focus/select the search input.
|
||||
|
||||
- **Rationale:** New custom hook and registry file. Implements
|
||||
`registerSearchInput` / `unregisterSearchInput` to manage mounted search input
|
||||
instances in a module-level `Set`. Implements `isTextEditable(event)` to guard
|
||||
against input hijacking across standard form controls (`<input>`,
|
||||
`<textarea>`, `<select>`, `<button>`) and ARIA text fields (`textbox`,
|
||||
`searchbox`, `spinbutton`, `combobox`) by traversing all nodes in the composed
|
||||
path. Implements `isModalActive(event)` to verify active visible modals
|
||||
(`dialog[open], .ui.modal.visible.active`) across main document and Shadow DOM
|
||||
roots (ignoring `aria-hidden="true"`). Implements `useSearchShortcut()` to
|
||||
attach a single window `keydown` listener (matching `event.key === '/'` with
|
||||
`AltGr` support), iterate registered inputs to identify the visible instance,
|
||||
intercept events with `preventDefault()`, and focus/select the search input.
|
||||
|
||||
#### 2. [search.tsx](file:///home/pwiech/personal/github/topola-viewer/src/menu/search.tsx)
|
||||
* **Rationale:** Remove `id="search"` to comply with HTML specs. Register and unregister the underlying `<input>` DOM node in `registeredSearchInputs` upon mount/unmount. Select between `"menu.search.placeholder"` and `"menu.search.placeholder_with_shortcut"` based on the `hideShortcutHint` prop. Add `aria-keyshortcuts="/"` to the input shorthand.
|
||||
|
||||
- **Rationale:** Remove `id="search"` to comply with HTML specs. Register and
|
||||
unregister the underlying `<input>` DOM node in `registeredSearchInputs` upon
|
||||
mount/unmount. Select between `"menu.search.placeholder"` and
|
||||
`"menu.search.placeholder_with_shortcut"` based on the `hideShortcutHint`
|
||||
prop. Add `aria-keyshortcuts="/"` to the input shorthand.
|
||||
|
||||
#### 3. [top_bar.tsx](file:///home/pwiech/personal/github/topola-viewer/src/menu/top_bar.tsx)
|
||||
* **Rationale:** Invoke `useSearchShortcut()` at the layout level. Pass `hideShortcutHint={true}` to the mobile `SearchBar` instance rendered inside `mobileMenus()`.
|
||||
|
||||
- **Rationale:** Invoke `useSearchShortcut()` at the layout level. Pass
|
||||
`hideShortcutHint={true}` to the mobile `SearchBar` instance rendered inside
|
||||
`mobileMenus()`.
|
||||
|
||||
#### 4. Localized Translation Files:
|
||||
* [de.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/de.json)
|
||||
* [pl.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/pl.json)
|
||||
* [fr.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/fr.json)
|
||||
* [it.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/it.json)
|
||||
* [ru.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/ru.json)
|
||||
* [bg.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/bg.json)
|
||||
* [cs.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/cs.json)
|
||||
* **Rationale:** Define `"menu.search.placeholder_with_shortcut"` with localized translations across all supported languages.
|
||||
|
||||
- [de.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/de.json)
|
||||
- [pl.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/pl.json)
|
||||
- [fr.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/fr.json)
|
||||
- [it.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/it.json)
|
||||
- [ru.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/ru.json)
|
||||
- [bg.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/bg.json)
|
||||
- [cs.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/cs.json)
|
||||
- **Rationale:** Define `"menu.search.placeholder_with_shortcut"` with localized
|
||||
translations across all supported languages.
|
||||
|
||||
#### 5. [search.spec.ts](file:///home/pwiech/personal/github/topola-viewer/tests/search.spec.ts)
|
||||
* **Rationale:** Playwright E2E test suite validating:
|
||||
* Shortcut focus and text selection upon pressing `/`.
|
||||
* Viewport switching where search query is preserved when resizing between desktop and mobile.
|
||||
* Collision safety when typing `/` into active modal inputs (e.g., "Load from URL").
|
||||
* Modal safety preventing focus theft when a modal is open and blurred.
|
||||
* Form control protection preventing focus theft when focused on buttons.
|
||||
* Modifier key exclusion (`Ctrl+/`, `Alt+/`, `Meta+/`).
|
||||
* Landing page safety where no search bar is rendered.
|
||||
* Question mark safety ensuring pressing `?` (`Shift+/`) does not trigger the shortcut.
|
||||
* Dropdown menu safety allowing the shortcut to focus the search bar while a dropdown menu is open.
|
||||
* Combobox wrapper interaction where clicking inside the search wrapper and pressing `/` focuses the input.
|
||||
* AltGr compatibility ensuring international keyboard combinations (`Ctrl+Alt+/`) trigger the shortcut.
|
||||
|
||||
- **Rationale:** Playwright E2E test suite validating:
|
||||
- Shortcut focus and text selection upon pressing `/`.
|
||||
- Viewport switching where search query is preserved when resizing between
|
||||
desktop and mobile.
|
||||
- Collision safety when typing `/` into active modal inputs (e.g., "Load from
|
||||
URL").
|
||||
- Modal safety preventing focus theft when a modal is open and blurred.
|
||||
- Form control protection preventing focus theft when focused on buttons.
|
||||
- Modifier key exclusion (`Ctrl+/`, `Alt+/`, `Meta+/`).
|
||||
- Landing page safety where no search bar is rendered.
|
||||
- Question mark safety ensuring pressing `?` (`Shift+/`) does not trigger the
|
||||
shortcut.
|
||||
- Dropdown menu safety allowing the shortcut to focus the search bar while a
|
||||
dropdown menu is open.
|
||||
- Combobox wrapper interaction where clicking inside the search wrapper and
|
||||
pressing `/` focuses the input.
|
||||
- AltGr compatibility ensuring international keyboard combinations
|
||||
(`Ctrl+Alt+/`) trigger the shortcut.
|
||||
|
||||
@@ -2,186 +2,381 @@
|
||||
|
||||
## Problem
|
||||
|
||||
When users upload genealogy data using GEDCOM ZIP (GDZ) files or by selecting local images alongside a GEDCOM file, the images display correctly on the family tree chart nodes. However, these same photos, as well as any other uploaded document attachments, fail to render in the details side panel and events list. This occurs because the side panel reads directly from the raw GEDCOM entries, which only contain relative local file paths that the browser cannot resolve. The goal of this feature is to pass the mapped local object URLs to the details panel and resolve these paths dynamically, ensuring a consistent and complete viewing experience for all uploaded media.
|
||||
When users upload genealogy data using GEDCOM ZIP (GDZ) files or by selecting
|
||||
local images alongside a GEDCOM file, the images display correctly on the family
|
||||
tree chart nodes. However, these same photos, as well as any other uploaded
|
||||
document attachments, fail to render in the details side panel and events list.
|
||||
This occurs because the side panel reads directly from the raw GEDCOM entries,
|
||||
which only contain relative local file paths that the browser cannot resolve.
|
||||
The goal of this feature is to pass the mapped local object URLs to the details
|
||||
panel and resolve these paths dynamically, ensuring a consistent and complete
|
||||
viewing experience for all uploaded media.
|
||||
|
||||
## The Technical Plan
|
||||
|
||||
To make uploaded images and files visible in the side panel, we will connect the loaded browser URL mapping to the components that display individual details and event lists.
|
||||
To make uploaded images and files visible in the side panel, we will connect the
|
||||
loaded browser URL mapping to the components that display individual details and
|
||||
event lists.
|
||||
|
||||
The plan involves four major areas:
|
||||
1. **Data Model Augmentation**: We will store the temporary file mapping (`images` map) in the main `TopolaData` container so it is available alongside the parsed GEDCOM data.
|
||||
2. **Propagating the Mapping**: The main `App` component holds the loaded `TopolaData`. We will pass the `images` map as a prop to the `SidePanel` component, which will in turn pass it down to `Details` and `Events`.
|
||||
3. **Resolving Relative Paths**: We will introduce a utility function that looks up relative paths or filenames in the `images` map. If a relative path is referenced in the GEDCOM, we will replace it with the resolved browser URL.
|
||||
4. **Permissive File Extraction**: We will modify the parser utility to stop filtering out non-web paths (paths that do not start with `http`), allowing the application to process relative file paths.
|
||||
|
||||
1. **Data Model Augmentation**: We will store the temporary file mapping
|
||||
(`images` map) in the main `TopolaData` container so it is available
|
||||
alongside the parsed GEDCOM data.
|
||||
2. **Propagating the Mapping**: The main `App` component holds the loaded
|
||||
`TopolaData`. We will pass the `images` map as a prop to the `SidePanel`
|
||||
component, which will in turn pass it down to `Details` and `Events`.
|
||||
3. **Resolving Relative Paths**: We will introduce a utility function that looks
|
||||
up relative paths or filenames in the `images` map. If a relative path is
|
||||
referenced in the GEDCOM, we will replace it with the resolved browser URL.
|
||||
4. **Permissive File Extraction**: We will modify the parser utility to stop
|
||||
filtering out non-web paths (paths that do not start with `http`), allowing
|
||||
the application to process relative file paths.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 1. Pre-resolving URLs Directly in the Raw GEDCOM Data Model
|
||||
We considered modifying the parsed `GedcomData` object to replace relative file paths with `blob:` URLs at load time, similar to how we pre-process `chartData`.
|
||||
* **Why Rejected**: `GedcomData` is designed to be a raw, immutable representation of the source GEDCOM file structure. Directly mutating this tree could break features that expect raw tag data (such as exports, raw view logs, or metadata parsing). Keeping the `images` resolver map separate maintains a clean separation of concerns and preserves the integrity of the original GEDCOM entries.
|
||||
|
||||
We considered modifying the parsed `GedcomData` object to replace relative file
|
||||
paths with `blob:` URLs at load time, similar to how we pre-process `chartData`.
|
||||
|
||||
- **Why Rejected**: `GedcomData` is designed to be a raw, immutable
|
||||
representation of the source GEDCOM file structure. Directly mutating this
|
||||
tree could break features that expect raw tag data (such as exports, raw view
|
||||
logs, or metadata parsing). Keeping the `images` resolver map separate
|
||||
maintains a clean separation of concerns and preserves the integrity of the
|
||||
original GEDCOM entries.
|
||||
|
||||
### 2. Base64-Encoding and Persisting Images in Session Storage
|
||||
We considered serializing the local file contents as Base64 strings so they could be saved in `sessionStorage` and survive page reloads.
|
||||
* **Why Rejected**: Browser `sessionStorage` is typically limited to 5MB. Genealogy ZIP archives can easily contain tens or hundreds of megabytes of media files, which would instantly exceed this quota and cause the application to crash. The performance cost of encoding and decoding large files in session storage is also prohibitive.
|
||||
|
||||
We considered serializing the local file contents as Base64 strings so they
|
||||
could be saved in `sessionStorage` and survive page reloads.
|
||||
|
||||
- **Why Rejected**: Browser `sessionStorage` is typically limited to 5MB.
|
||||
Genealogy ZIP archives can easily contain tens or hundreds of megabytes of
|
||||
media files, which would instantly exceed this quota and cause the application
|
||||
to crash. The performance cost of encoding and decoding large files in session
|
||||
storage is also prohibitive.
|
||||
|
||||
### 3. Rendering Placeholders for Unresolved Local Media References
|
||||
We considered rendering warning cards or broken image placeholders for relative paths that cannot be resolved (i.e., when a GEDCOM lists a photo that wasn't included in the uploaded files/ZIP).
|
||||
* **Why Rejected**: GEDCOM files created by desktop software often contain references to hundreds of local photos stored on the user's computer. Since users typically upload only the GEDCOM file (or a zip containing a subset of files), displaying error panels or placeholders for every missing image would clutter the UI. Returning `null` to gracefully hide missing media keeps the side panel clean and focused on available information.
|
||||
|
||||
We considered rendering warning cards or broken image placeholders for relative
|
||||
paths that cannot be resolved (i.e., when a GEDCOM lists a photo that wasn't
|
||||
included in the uploaded files/ZIP).
|
||||
|
||||
- **Why Rejected**: GEDCOM files created by desktop software often contain
|
||||
references to hundreds of local photos stored on the user's computer. Since
|
||||
users typically upload only the GEDCOM file (or a zip containing a subset of
|
||||
files), displaying error panels or placeholders for every missing image would
|
||||
clutter the UI. Returning `null` to gracefully hide missing media keeps the
|
||||
side panel clean and focused on available information.
|
||||
|
||||
### 4. Storing both full relative path and base filename in the `images` map
|
||||
We considered storing files under both their full path (e.g. `photos/person.jpg`) and their base filename (e.g. `person.jpg`) inside the `images` map to handle cases where the GEDCOM file does not preserve the folder name.
|
||||
* **Why Rejected**: We assume the GEDCOM file references files preserving the folder name as exported by the source application. Storing both keys could also lead to collisions if two different folders contain files with the same name.
|
||||
|
||||
We considered storing files under both their full path (e.g.
|
||||
`photos/person.jpg`) and their base filename (e.g. `person.jpg`) inside the
|
||||
`images` map to handle cases where the GEDCOM file does not preserve the folder
|
||||
name.
|
||||
|
||||
- **Why Rejected**: We assume the GEDCOM file references files preserving the
|
||||
folder name as exported by the source application. Storing both keys could
|
||||
also lead to collisions if two different folders contain files with the same
|
||||
name.
|
||||
|
||||
## Detailed Implementation
|
||||
|
||||
This section lists every file that will be created or changed, the step-by-step changes, and the technical rationale.
|
||||
This section lists every file that will be created or changed, the step-by-step
|
||||
changes, and the technical rationale.
|
||||
|
||||
### 1. [gedcom_util.ts](../src/util/gedcom_util.ts)
|
||||
|
||||
#### Proposed Changes:
|
||||
* Update `TopolaData` interface to add `images?: Map<string, string>;`.
|
||||
* In `convertGedcom`, return the input `images` map in the output object.
|
||||
* Remove `entry.data.startsWith('http')` check inside `findFileEntry` and ensure `entry.data` is a non-empty string before running predicate checks to avoid crashes.
|
||||
* Add `.webp` to the `IMAGE_EXTENSIONS` list to support modern image formats.
|
||||
* Update `isImageFile` to strip query parameters (`?`) and hashes (`#`) from the path/URL before verifying the extension.
|
||||
* Centralize filename extraction inside `getFileName`: if `TITL` or `FORM` is missing, extract the filename from `fileEntry.data` (normalizing backslashes to forward slashes and stripping query parameters or hashes first).
|
||||
* Implement and export `isBrowserLoadable(url: string): boolean` which returns `true` if the URL starts with `http://`, `https://`, `blob:`, `data:`, or `//`.
|
||||
* Implement and export `resolveFileUrl(url: string, images?: Map<string, string>): string`. This function:
|
||||
1. Returns the input URL immediately if it is browser-loadable (checked via `isBrowserLoadable`).
|
||||
|
||||
- Update `TopolaData` interface to add `images?: Map<string, string>;`.
|
||||
- In `convertGedcom`, return the input `images` map in the output object.
|
||||
- Remove `entry.data.startsWith('http')` check inside `findFileEntry` and ensure
|
||||
`entry.data` is a non-empty string before running predicate checks to avoid
|
||||
crashes.
|
||||
- Add `.webp` to the `IMAGE_EXTENSIONS` list to support modern image formats.
|
||||
- Update `isImageFile` to strip query parameters (`?`) and hashes (`#`) from the
|
||||
path/URL before verifying the extension.
|
||||
- Centralize filename extraction inside `getFileName`: if `TITL` or `FORM` is
|
||||
missing, extract the filename from `fileEntry.data` (normalizing backslashes
|
||||
to forward slashes and stripping query parameters or hashes first).
|
||||
- Implement and export `isBrowserLoadable(url: string): boolean` which returns
|
||||
`true` if the URL starts with `http://`, `https://`, `blob:`, `data:`, or
|
||||
`//`.
|
||||
- Implement and export
|
||||
`resolveFileUrl(url: string, images?: Map<string, string>): string`. This
|
||||
function:
|
||||
1. Returns the input URL immediately if it is browser-loadable (checked via
|
||||
`isBrowserLoadable`).
|
||||
2. Replaces all backslashes (`\`) with forward slashes (`/`).
|
||||
3. Checks the `images` map for the normalized lowercase path first, then for the lowercase base filename.
|
||||
4. Returns the mapped `blob:` URL if found; otherwise, returns the original normalized URL.
|
||||
* *Robustness check*: The function will safely check if `images` is a valid `Map` (using `images instanceof Map`) to avoid runtime errors when data is loaded from session storage.
|
||||
* Refactor `getImageFileEntry` and `getNonImageFileEntry` to `getImageFileEntries` and `getNonImageFileEntries` to return all matching file entries within an `OBJE` tag, supporting multimedia objects with multiple file attachments.
|
||||
* Update `filterImage` inside `gedcom_util.ts` to resolve the URL using the new `resolveFileUrl` helper, ensuring case-insensitive lookups for chart node images.
|
||||
3. Checks the `images` map for the normalized lowercase path first, then for
|
||||
the lowercase base filename.
|
||||
4. Returns the mapped `blob:` URL if found; otherwise, returns the original
|
||||
normalized URL.
|
||||
- _Robustness check_: The function will safely check if `images` is a valid
|
||||
`Map` (using `images instanceof Map`) to avoid runtime errors when data is
|
||||
loaded from session storage.
|
||||
- Refactor `getImageFileEntry` and `getNonImageFileEntry` to
|
||||
`getImageFileEntries` and `getNonImageFileEntries` to return all matching file
|
||||
entries within an `OBJE` tag, supporting multimedia objects with multiple file
|
||||
attachments.
|
||||
- Update `filterImage` inside `gedcom_util.ts` to resolve the URL using the new
|
||||
`resolveFileUrl` helper, ensuring case-insensitive lookups for chart node
|
||||
images.
|
||||
|
||||
#### Rationale:
|
||||
Extending the model interface `TopolaData` binds the lifetime of the `images` map to the active loaded dataset. Removing `startsWith('http')` allows extracting relative paths. Centralizing the loadability check, URL decoding, case normalization, and base filename extraction helpers avoids code duplication. Supporting multiple file entries per `OBJE` record aligns with the GEDCOM multimedia specifications.
|
||||
|
||||
Extending the model interface `TopolaData` binds the lifetime of the `images`
|
||||
map to the active loaded dataset. Removing `startsWith('http')` allows
|
||||
extracting relative paths. Centralizing the loadability check, URL decoding,
|
||||
case normalization, and base filename extraction helpers avoids code
|
||||
duplication. Supporting multiple file entries per `OBJE` record aligns with the
|
||||
GEDCOM multimedia specifications.
|
||||
|
||||
### 2. [load_data.ts](../src/datasource/load_data.ts)
|
||||
|
||||
#### Proposed Changes:
|
||||
* In `prepareData`, pass the `images` map parameter to `convertGedcom` and return it inside the resulting data object.
|
||||
* Ensure that the `images` map is *not* stored in `sessionStorage` by explicitly deleting or omitting the `images` key before calling `JSON.stringify(data)` in `prepareData`. This guarantees that `data.images` will be `undefined` (and not a plain empty object `{}`) when restored from `sessionStorage`.
|
||||
* In `loadGedzip`, store the unzipped files in the `images` map using lowercase keys and normalizing backslashes to forward slashes to ensure case-insensitive lookup.
|
||||
* Wrap the unzipping/extraction loop in `loadGedzip` in a `try-catch` block. If extraction fails or no GEDCOM file is found, revoke all `blob:` URLs created so far in the loop.
|
||||
* Implement `try-catch` cleanup blocks in both `loadGedcom` and `loadFromUrl` to ensure that if file loading or parsing throws an error, all `blob:` URLs in the `images` map are immediately revoked to prevent memory leaks.
|
||||
|
||||
- In `prepareData`, pass the `images` map parameter to `convertGedcom` and
|
||||
return it inside the resulting data object.
|
||||
- Ensure that the `images` map is _not_ stored in `sessionStorage` by explicitly
|
||||
deleting or omitting the `images` key before calling `JSON.stringify(data)` in
|
||||
`prepareData`. This guarantees that `data.images` will be `undefined` (and not
|
||||
a plain empty object `{}`) when restored from `sessionStorage`.
|
||||
- In `loadGedzip`, store the unzipped files in the `images` map using lowercase
|
||||
keys and normalizing backslashes to forward slashes to ensure case-insensitive
|
||||
lookup.
|
||||
- Wrap the unzipping/extraction loop in `loadGedzip` in a `try-catch` block. If
|
||||
extraction fails or no GEDCOM file is found, revoke all `blob:` URLs created
|
||||
so far in the loop.
|
||||
- Implement `try-catch` cleanup blocks in both `loadGedcom` and `loadFromUrl` to
|
||||
ensure that if file loading or parsing throws an error, all `blob:` URLs in
|
||||
the `images` map are immediately revoked to prevent memory leaks.
|
||||
|
||||
#### Rationale:
|
||||
Connects the ingestion pipeline to the `TopolaData` model. Deleting `images` prior to `sessionStorage` serialization prevents restoring a broken plain object `{}`. Adding try-catch blocks to cleanup `images` on loading or parsing failures prevents memory leaks from unrevoked `blob:` URLs.
|
||||
|
||||
Connects the ingestion pipeline to the `TopolaData` model. Deleting `images`
|
||||
prior to `sessionStorage` serialization prevents restoring a broken plain object
|
||||
`{}`. Adding try-catch blocks to cleanup `images` on loading or parsing failures
|
||||
prevents memory leaks from unrevoked `blob:` URLs.
|
||||
|
||||
### 3. [side-panel.tsx](../src/sidepanel/side-panel.tsx)
|
||||
|
||||
#### Proposed Changes:
|
||||
* Locate `<Details>` rendering block inside the `tabs` definition.
|
||||
* Pass the map to the details panel: `images={data.images}`.
|
||||
|
||||
- Locate `<Details>` rendering block inside the `tabs` definition.
|
||||
- Pass the map to the details panel: `images={data.images}`.
|
||||
|
||||
#### Rationale:
|
||||
Acts as the intermediary component that forwards data properties from the root application state to the details view pane.
|
||||
|
||||
Acts as the intermediary component that forwards data properties from the root
|
||||
application state to the details view pane.
|
||||
|
||||
### 4. [details.tsx](../src/sidepanel/details/details.tsx)
|
||||
|
||||
#### Proposed Changes:
|
||||
* Update the `Props` interface for `Details` to accept `images?: Map<string, string>;`.
|
||||
* Pass `props.images` to `imageDetails` and `fileDetails` (using inline arrow functions inside `getSectionForEachMatchingEntry` and `combineAllMatchingEntriesIntoSingleSection`), and `<Events ... images={props.images} />`.
|
||||
* In `imageDetails`, retrieve all image file entries using `getImageFileEntries`. For each, resolve the URL using `resolveFileUrl(..., images)` and render `WrappedImage` with the resolved URL and its filename.
|
||||
* In `fileDetails`, retrieve all non-image file entries using `getNonImageFileEntries`. For each, resolve the URL using `resolveFileUrl(..., images)` and pass the resolved URLs to `AdditionalFiles`.
|
||||
|
||||
- Update the `Props` interface for `Details` to accept
|
||||
`images?: Map<string, string>;`.
|
||||
- Pass `props.images` to `imageDetails` and `fileDetails` (using inline arrow
|
||||
functions inside `getSectionForEachMatchingEntry` and
|
||||
`combineAllMatchingEntriesIntoSingleSection`), and
|
||||
`<Events ... images={props.images} />`.
|
||||
- In `imageDetails`, retrieve all image file entries using
|
||||
`getImageFileEntries`. For each, resolve the URL using
|
||||
`resolveFileUrl(..., images)` and render `WrappedImage` with the resolved URL
|
||||
and its filename.
|
||||
- In `fileDetails`, retrieve all non-image file entries using
|
||||
`getNonImageFileEntries`. For each, resolve the URL using
|
||||
`resolveFileUrl(..., images)` and pass the resolved URLs to `AdditionalFiles`.
|
||||
|
||||
#### Rationale:
|
||||
Directly resolves image and non-image document URLs from the loaded files list. Iterating over all resolved files from `getImageFileEntries` and `getNonImageFileEntries` supports multiple attachments per object record.
|
||||
|
||||
Directly resolves image and non-image document URLs from the loaded files list.
|
||||
Iterating over all resolved files from `getImageFileEntries` and
|
||||
`getNonImageFileEntries` supports multiple attachments per object record.
|
||||
|
||||
### 5. [events.tsx](../src/sidepanel/details/events.tsx)
|
||||
|
||||
#### Proposed Changes:
|
||||
* Update the `Props` interface for `Events` to accept `images?: Map<string, string>;`.
|
||||
* Pass `images` to `toEvent` -> `toIndiEvent` & `toFamilyEvents` -> `eventImages` & `eventFiles`.
|
||||
* In `eventImages(entry, gedcom, images)`, extract all image files using `getImageFileEntries`. For each, resolve the URL using `resolveFileUrl` and add to the returned array.
|
||||
* In `eventFiles(entry, gedcom, images)`, extract all non-image files using `getNonImageFileEntries`. For each, resolve the URL using `resolveFileUrl` and add to the returned array.
|
||||
|
||||
- Update the `Props` interface for `Events` to accept
|
||||
`images?: Map<string, string>;`.
|
||||
- Pass `images` to `toEvent` -> `toIndiEvent` & `toFamilyEvents` ->
|
||||
`eventImages` & `eventFiles`.
|
||||
- In `eventImages(entry, gedcom, images)`, extract all image files using
|
||||
`getImageFileEntries`. For each, resolve the URL using `resolveFileUrl` and
|
||||
add to the returned array.
|
||||
- In `eventFiles(entry, gedcom, images)`, extract all non-image files using
|
||||
`getNonImageFileEntries`. For each, resolve the URL using `resolveFileUrl` and
|
||||
add to the returned array.
|
||||
|
||||
#### Rationale:
|
||||
Allows life events to load, resolve, and render all associated media files, supporting multiple files and case-insensitive resolution.
|
||||
|
||||
Allows life events to load, resolve, and render all associated media files,
|
||||
supporting multiple files and case-insensitive resolution.
|
||||
|
||||
### 6. [additional-files.tsx](../src/sidepanel/details/additional-files.tsx)
|
||||
|
||||
#### Proposed Changes:
|
||||
* Map file extensions to Semantic UI icons (e.g. `file pdf outline` for PDF, `file image outline` for images, etc.) to improve the design aesthetics.
|
||||
* Add the `download` attribute to the `<a>` tag for `blob:` URLs to trigger a file download instead of triggering browser security blocks on top-level navigations:
|
||||
|
||||
- Map file extensions to Semantic UI icons (e.g. `file pdf outline` for PDF,
|
||||
`file image outline` for images, etc.) to improve the design aesthetics.
|
||||
- Add the `download` attribute to the `<a>` tag for `blob:` URLs to trigger a
|
||||
file download instead of triggering browser security blocks on top-level
|
||||
navigations:
|
||||
```typescript
|
||||
download={file.url.startsWith('blob:') ? file.filename || true : undefined}
|
||||
```
|
||||
* Update `AdditionalFiles` component to check if the file is browser-loadable using `isBrowserLoadable(file.url)`. For non-loadable URLs:
|
||||
- Update `AdditionalFiles` component to check if the file is browser-loadable
|
||||
using `isBrowserLoadable(file.url)`. For non-loadable URLs:
|
||||
- Do not render the `<a>` link (thus preventing download/navigation actions).
|
||||
- Render the filename as grayed-out plain text with an italicized `(File not uploaded)` helper message.
|
||||
- Render the filename as grayed-out plain text with an italicized
|
||||
`(File not uploaded)` helper message.
|
||||
|
||||
#### Rationale:
|
||||
Implements premium file icon layouts based on file types. Adding the `download` attribute is critical to bypass browser-level blocks on opening `blob:` URLs in new tabs. Checking loadability prevents users from navigating to broken local paths.
|
||||
|
||||
Implements premium file icon layouts based on file types. Adding the `download`
|
||||
attribute is critical to bypass browser-level blocks on opening `blob:` URLs in
|
||||
new tabs. Checking loadability prevents users from navigating to broken local
|
||||
paths.
|
||||
|
||||
### 7. [wrapped-image.tsx](../src/sidepanel/details/wrapped-image.tsx)
|
||||
|
||||
#### Proposed Changes:
|
||||
* Update `WrappedImage` to check if `props.url` is browser-loadable using `isBrowserLoadable`. If not, immediately render a clean gray card placeholder (displaying the image icon, the title or filename, and a "File not uploaded" badge) without rendering the `<img>` tag or trying to load the image.
|
||||
|
||||
- Update `WrappedImage` to check if `props.url` is browser-loadable using
|
||||
`isBrowserLoadable`. If not, immediately render a clean gray card placeholder
|
||||
(displaying the image icon, the title or filename, and a "File not uploaded"
|
||||
badge) without rendering the `<img>` tag or trying to load the image.
|
||||
|
||||
#### Rationale:
|
||||
Prevents browser 404/CORS console errors when attempting to load raw local relative paths as images, presenting a consistent and clean "File not uploaded" message to the user instead.
|
||||
|
||||
Prevents browser 404/CORS console errors when attempting to load raw local
|
||||
relative paths as images, presenting a consistent and clean "File not uploaded"
|
||||
message to the user instead.
|
||||
|
||||
### 8. [app.tsx](../src/app.tsx)
|
||||
|
||||
#### Proposed Changes:
|
||||
* Add a `useEffect` cleanup hook in the `App` component that monitors changes to the active `data` state. When `data` changes, it iterates over the old `data.images` map and calls `URL.revokeObjectURL(url)` for all `blob:` URLs.
|
||||
* In the path routing `useEffect` of the `App` component, if the path is not `/view`, explicitly reset the `data` state to `undefined`. This triggers the cleanup hook to revoke all active `blob:` URLs when leaving the chart viewer (e.g. going back to the landing page).
|
||||
|
||||
- Add a `useEffect` cleanup hook in the `App` component that monitors changes to
|
||||
the active `data` state. When `data` changes, it iterates over the old
|
||||
`data.images` map and calls `URL.revokeObjectURL(url)` for all `blob:` URLs.
|
||||
- In the path routing `useEffect` of the `App` component, if the path is not
|
||||
`/view`, explicitly reset the `data` state to `undefined`. This triggers the
|
||||
cleanup hook to revoke all active `blob:` URLs when leaving the chart viewer
|
||||
(e.g. going back to the landing page).
|
||||
|
||||
#### Rationale:
|
||||
Prevents browser memory leaks from unrevoked object URLs when users upload and switch between different genealogy files, or navigate away from the viewer.
|
||||
|
||||
Prevents browser memory leaks from unrevoked object URLs when users upload and
|
||||
switch between different genealogy files, or navigate away from the viewer.
|
||||
|
||||
### 9. [upload_menu.tsx](../src/menu/upload_menu.tsx)
|
||||
|
||||
#### Proposed Changes:
|
||||
* Update the file input `accept` attribute to allow selecting `.webp` images.
|
||||
* Remove the local `isImageFileName` helper function and import `isImageFile` from `gedcom_util.ts` to ensure consistent file extension support.
|
||||
* Convert individually uploaded filenames to lowercase when storing them as keys in the `images` map to ensure case-insensitive matching:
|
||||
|
||||
- Update the file input `accept` attribute to allow selecting `.webp` images.
|
||||
- Remove the local `isImageFileName` helper function and import `isImageFile`
|
||||
from `gedcom_util.ts` to ensure consistent file extension support.
|
||||
- Convert individually uploaded filenames to lowercase when storing them as keys
|
||||
in the `images` map to ensure case-insensitive matching:
|
||||
```typescript
|
||||
images.set(file.name.toLowerCase(), URL.createObjectURL(file));
|
||||
```
|
||||
|
||||
#### Rationale:
|
||||
Extends modern image format support and unifies the image extension logic across the codebase. Storing keys as lowercase enables case-insensitive lookups for individually uploaded files.
|
||||
|
||||
Extends modern image format support and unifies the image extension logic across
|
||||
the codebase. Storing keys as lowercase enables case-insensitive lookups for
|
||||
individually uploaded files.
|
||||
|
||||
### 10. [Translation JSON Files](../src/translations/)
|
||||
|
||||
#### Proposed Changes:
|
||||
* Add the localization key `"media.not_uploaded"` to all translation files (`bg.json`, `cs.json`, `de.json`, `fr.json`, `it.json`, `pl.json`, `ru.json`).
|
||||
|
||||
- Add the localization key `"media.not_uploaded"` to all translation files
|
||||
(`bg.json`, `cs.json`, `de.json`, `fr.json`, `it.json`, `pl.json`, `ru.json`).
|
||||
|
||||
#### Rationale:
|
||||
Ensures the new "File not uploaded" text badge is localized in all supported languages of the application.
|
||||
|
||||
Ensures the new "File not uploaded" text badge is localized in all supported
|
||||
languages of the application.
|
||||
|
||||
## Verification Plan
|
||||
|
||||
We will verify the changes using automated unit tests, end-to-end regression checks, and manual visual testing.
|
||||
We will verify the changes using automated unit tests, end-to-end regression
|
||||
checks, and manual visual testing.
|
||||
|
||||
### Automated Unit Tests
|
||||
|
||||
We will run and add unit tests to ensure correct data resolution:
|
||||
1. **Existing Test Verification**: Run `npm test` to verify all current unit tests pass.
|
||||
2. **`resolveFileUrl` Unit Tests**: Add tests in `src/util/gedcom_util.spec.ts` to verify:
|
||||
- Resolving a relative path that matches a key in the `images` map returns the correct `blob:` URL.
|
||||
- Resolving a filename that matches a key in the `images` map returns the correct `blob:` URL.
|
||||
- Resolving a URL that doesn't exist in the `images` map returns the original URL unchanged.
|
||||
3. **`findFileEntry` Permissive Extract**: Add tests in `src/util/gedcom_util.spec.ts` to verify that `findFileEntry` matches and returns local relative `FILE` entries (e.g., `images/photo.jpg`), which were previously filtered out.
|
||||
4. **Session cache recovery verification**: Add a test that verifies reloading the page correctly loads data from `sessionStorage` when `data.images` is `undefined`, ensuring no crashes occur.
|
||||
|
||||
1. **Existing Test Verification**: Run `npm test` to verify all current unit
|
||||
tests pass.
|
||||
2. **`resolveFileUrl` Unit Tests**: Add tests in `src/util/gedcom_util.spec.ts`
|
||||
to verify:
|
||||
- Resolving a relative path that matches a key in the `images` map returns
|
||||
the correct `blob:` URL.
|
||||
- Resolving a filename that matches a key in the `images` map returns the
|
||||
correct `blob:` URL.
|
||||
- Resolving a URL that doesn't exist in the `images` map returns the original
|
||||
URL unchanged.
|
||||
3. **`findFileEntry` Permissive Extract**: Add tests in
|
||||
`src/util/gedcom_util.spec.ts` to verify that `findFileEntry` matches and
|
||||
returns local relative `FILE` entries (e.g., `images/photo.jpg`), which were
|
||||
previously filtered out.
|
||||
4. **Session cache recovery verification**: Add a test that verifies reloading
|
||||
the page correctly loads data from `sessionStorage` when `data.images` is
|
||||
`undefined`, ensuring no crashes occur.
|
||||
|
||||
### Playwright Visual & E2E Tests
|
||||
|
||||
We will add automated verification of the upload flow and visual rendering:
|
||||
1. **Add visual test**: Create a new test case in `tests/details_visual.spec.ts`:
|
||||
|
||||
1. **Add visual test**: Create a new test case in
|
||||
`tests/details_visual.spec.ts`:
|
||||
- Start at `/`.
|
||||
- Setup Playwright's `filechooser` listener.
|
||||
- Trigger the file upload menu, select and upload the `src/datasource/testdata/test.gdz` file.
|
||||
- Trigger the file upload menu, select and upload the
|
||||
`src/datasource/testdata/test.gdz` file.
|
||||
- Click the node for the individual that contains the photo (e.g., Radobod).
|
||||
- Wait for the side panel to open and the image to load.
|
||||
- Match sidebar screenshot against a visual regression baseline (`details-uploaded-gdz-photo.png`).
|
||||
2. **Add missing file placeholder test**: Verify that if a linked attachment is missing, a "File not uploaded" label or placeholder card renders in the details pane, matching a baseline screenshot.
|
||||
- Match sidebar screenshot against a visual regression baseline
|
||||
(`details-uploaded-gdz-photo.png`).
|
||||
2. **Add missing file placeholder test**: Verify that if a linked attachment is
|
||||
missing, a "File not uploaded" label or placeholder card renders in the
|
||||
details pane, matching a baseline screenshot.
|
||||
3. Run `npm run test:visual` to confirm tests execute and pass.
|
||||
|
||||
### Manual / Visual Verification
|
||||
|
||||
We will manually verify the visual rendering on a local development server:
|
||||
|
||||
1. Run `npm run dev` to launch the local application.
|
||||
2. Prepare a test GDZ file: Zip the contents of the `docker/examples/photos` directory (which contains `family.ged` referencing relative photo paths under a `photos` subfolder).
|
||||
2. Prepare a test GDZ file: Zip the contents of the `docker/examples/photos`
|
||||
directory (which contains `family.ged` referencing relative photo paths under
|
||||
a `photos` subfolder).
|
||||
3. Upload the resulting GDZ file to the application.
|
||||
4. Verify that:
|
||||
- The primary individual's photo displays correctly in the side panel Details tab.
|
||||
- Any documents or images linked to events (such as birth or marriage events) are rendered correctly in the event info cards.
|
||||
- GEDCOM files referencing missing images render "File not uploaded" placeholders gracefully instead of breaking or showing broken image icons.
|
||||
- The primary individual's photo displays correctly in the side panel Details
|
||||
tab.
|
||||
- Any documents or images linked to events (such as birth or marriage events)
|
||||
are rendered correctly in the event info cards.
|
||||
- GEDCOM files referencing missing images render "File not uploaded"
|
||||
placeholders gracefully instead of breaking or showing broken image icons.
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### IndexedDB for Upload Persistence
|
||||
Currently, we reject persisting images in `sessionStorage` due to the 5MB size limit. In the future, we could consider using **IndexedDB** to store the zipped `.gdz` archives or the extracted file blobs. Since IndexedDB supports much larger storage quotas (typically hundreds of megabytes or more), this would allow uploaded genealogy trees and their media attachments to persist across page reloads and browser restarts, providing a much smoother user experience.
|
||||
|
||||
Currently, we reject persisting images in `sessionStorage` due to the 5MB size
|
||||
limit. In the future, we could consider using **IndexedDB** to store the zipped
|
||||
`.gdz` archives or the extracted file blobs. Since IndexedDB supports much
|
||||
larger storage quotas (typically hundreds of megabytes or more), this would
|
||||
allow uploaded genealogy trees and their media attachments to persist across
|
||||
page reloads and browser restarts, providing a much smoother user experience.
|
||||
|
||||
@@ -1,14 +1,33 @@
|
||||
# WebMCP Interface Design
|
||||
|
||||
## The Problem
|
||||
Exploring large genealogy charts can be overwhelming and time-consuming because users have to manually search, click, and scroll through hundreds of interconnected family branches just to find simple answers. To make this experience intuitive and modern, we are adding an interactive AI layer that acts as a research copilot directly inside the browser. This will allow users to effortlessly ask questions like "how is John related to Mary" or command the map to focus on specific relatives using simple natural conversation. Ultimately, this makes genealogy research accessible to everyone, letting users engage with their ancestry without wrestling with complex navigation controls.
|
||||
|
||||
Exploring large genealogy charts can be overwhelming and time-consuming because
|
||||
users have to manually search, click, and scroll through hundreds of
|
||||
interconnected family branches just to find simple answers. To make this
|
||||
experience intuitive and modern, we are adding an interactive AI layer that acts
|
||||
as a research copilot directly inside the browser. This will allow users to
|
||||
effortlessly ask questions like "how is John related to Mary" or command the map
|
||||
to focus on specific relatives using simple natural conversation. Ultimately,
|
||||
this makes genealogy research accessible to everyone, letting users engage with
|
||||
their ancestry without wrestling with complex navigation controls.
|
||||
|
||||
## System Architecture (How it works)
|
||||
To make this feature work, we bridge three simple components together to let the map and the Assistant communicate smoothly:
|
||||
|
||||
1. **The Visual Chart (Topola core):** This is what you see on your screen. It draws the family members, sets up transitions, and tracks who you are currently looking at.
|
||||
2. **The Assistant Adapter (The Bridge):** A singleton instance class (`WebMcpBridge`) instantiated once in `App.tsx` and running in the background. It acts as a continuous translator, giving the assistant access to read and move the active user chart while preventing disconnected side variables and state memory leaks.
|
||||
3. **The AI Command Registry (WebMCP):** The external plug that allows the browser AI to issue predefined commands (such as "Focus on Sarah" or "Get direct descendants") to the Assistant Adapter.
|
||||
To make this feature work, we bridge three simple components together to let the
|
||||
map and the Assistant communicate smoothly:
|
||||
|
||||
1. **The Visual Chart (Topola core):** This is what you see on your screen. It
|
||||
draws the family members, sets up transitions, and tracks who you are
|
||||
currently looking at.
|
||||
2. **The Assistant Adapter (The Bridge):** A singleton instance class
|
||||
(`WebMcpBridge`) instantiated once in `App.tsx` and running in the
|
||||
background. It acts as a continuous translator, giving the assistant access
|
||||
to read and move the active user chart while preventing disconnected side
|
||||
variables and state memory leaks.
|
||||
3. **The AI Command Registry (WebMCP):** The external plug that allows the
|
||||
browser AI to issue predefined commands (such as "Focus on Sarah" or "Get
|
||||
direct descendants") to the Assistant Adapter.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
@@ -17,100 +36,128 @@ graph LR
|
||||
Bridge <--> Assistant["AI Browser Assistant"]
|
||||
```
|
||||
|
||||
This setup lets the AI control the tree smoothly without rebuilding the viewer from scratch. It behaves much like a second pair of hands working beside you on the same controls.
|
||||
This setup lets the AI control the tree smoothly without rebuilding the viewer
|
||||
from scratch. It behaves much like a second pair of hands working beside you on
|
||||
the same controls.
|
||||
|
||||
## Rejected Alternatives (Design Guardrails)
|
||||
To ensure consistent development and avoid repeating past defaults, here are the alternate designs considered but discarded during early setup:
|
||||
|
||||
* **Pushing real-time events for layout clicks:** We evaluated building a reactive push model that constantly updates the AI on every user click in real-time. We rejected this in favor of passive standard queries (`get_selected_person()`) because constant pushes can confuse the assistant and bloat the UI event stream.
|
||||
* **A single combined Focus & Details tool:** We considered having one command perform both view manipulation and metadata inspection. We split these into isolated fetch commands (`inspect_indi`) and camera viewport shifts (`focus_indi`) so reading relatives stays fast and doesn't accidentally jerk the user's screen viewpoint.
|
||||
* **Relying solely on recursive tool loops for deep trees:** Initially, direct single-node queries were considered enough for relationships. We rejected leaving the AI to fetch single nodes repeatedly in favor of generational acceleration commands (`get_ancestors` bounded to 5 generations ceiling) to protect interactive performance latency.
|
||||
* **Internal fuzzy date parser algorithms:** We decided against writing standard regex parser logic for partial or approximate genealogy records (e.g., `"ABT 1750"`, `"BEFORE 1800"`). Instead, raw in-flight text string dumps allow standard conversational LLMs to contextualize approximation by themselves.
|
||||
* **Module-scoped global state variables:** Rejected in favor of a single static bridge instance (Singleton pattern) initialized inside the React application frame. This prevents loose standard closures and allows isolated updates for new file uploads.
|
||||
* **Custom synchronous DOM events for state queries:** Considered dispatching events from tools and capturing them inside React components. Rejected due to overhead constraints and event emitter latency.
|
||||
To ensure consistent development and avoid repeating past defaults, here are the
|
||||
alternate designs considered but discarded during early setup:
|
||||
|
||||
- **Pushing real-time events for layout clicks:** We evaluated building a
|
||||
reactive push model that constantly updates the AI on every user click in
|
||||
real-time. We rejected this in favor of passive standard queries
|
||||
(`get_selected_person()`) because constant pushes can confuse the assistant
|
||||
and bloat the UI event stream.
|
||||
- **A single combined Focus & Details tool:** We considered having one command
|
||||
perform both view manipulation and metadata inspection. We split these into
|
||||
isolated fetch commands (`inspect_indi`) and camera viewport shifts
|
||||
(`focus_indi`) so reading relatives stays fast and doesn't accidentally jerk
|
||||
the user's screen viewpoint.
|
||||
- **Relying solely on recursive tool loops for deep trees:** Initially, direct
|
||||
single-node queries were considered enough for relationships. We rejected
|
||||
leaving the AI to fetch single nodes repeatedly in favor of generational
|
||||
acceleration commands (`get_ancestors` bounded to 5 generations ceiling) to
|
||||
protect interactive performance latency.
|
||||
- **Internal fuzzy date parser algorithms:** We decided against writing standard
|
||||
regex parser logic for partial or approximate genealogy records (e.g.,
|
||||
`"ABT 1750"`, `"BEFORE 1800"`). Instead, raw in-flight text string dumps allow
|
||||
standard conversational LLMs to contextualize approximation by themselves.
|
||||
- **Module-scoped global state variables:** Rejected in favor of a single static
|
||||
bridge instance (Singleton pattern) initialized inside the React application
|
||||
frame. This prevents loose standard closures and allows isolated updates for
|
||||
new file uploads.
|
||||
- **Custom synchronous DOM events for state queries:** Considered dispatching
|
||||
events from tools and capturing them inside React components. Rejected due to
|
||||
overhead constraints and event emitter latency.
|
||||
|
||||
## Exposed MCP Tools
|
||||
|
||||
To enable smooth AI communication, the following tools are exposed to the LLM. Returned individual details are structured into three level tiers:
|
||||
To enable smooth AI communication, the following tools are exposed to the LLM.
|
||||
Returned individual details are structured into three level tiers:
|
||||
|
||||
- **`IndiReference`:**
|
||||
|
||||
* **`IndiReference`:**
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" }
|
||||
"id": {"type": "string"},
|
||||
"name": {"type": "string"}
|
||||
},
|
||||
"required": ["id", "name"]
|
||||
}
|
||||
```
|
||||
|
||||
* **`BasicIndi`:**
|
||||
- **`BasicIndi`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"id": {"type": "string"},
|
||||
"name": {"type": "string"},
|
||||
"birth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": { "type": "string" },
|
||||
"place": { "type": "string" }
|
||||
"date": {"type": "string"},
|
||||
"place": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"death": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": { "type": "string" },
|
||||
"place": { "type": "string" }
|
||||
"date": {"type": "string"},
|
||||
"place": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"mother": { "$ref": "#/definitions/IndiReference" },
|
||||
"father": { "$ref": "#/definitions/IndiReference" }
|
||||
"mother": {"$ref": "#/definitions/IndiReference"},
|
||||
"father": {"$ref": "#/definitions/IndiReference"}
|
||||
},
|
||||
"required": ["id", "name"]
|
||||
}
|
||||
```
|
||||
|
||||
* **`FullIndi`:**
|
||||
- **`FullIndi`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"id": {"type": "string"},
|
||||
"name": {"type": "string"},
|
||||
"birth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": { "type": "string" },
|
||||
"place": { "type": "string" }
|
||||
"date": {"type": "string"},
|
||||
"place": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"death": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": { "type": "string" },
|
||||
"place": { "type": "string" }
|
||||
"date": {"type": "string"},
|
||||
"place": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"mother": { "$ref": "#/definitions/BasicIndi" },
|
||||
"father": { "$ref": "#/definitions/BasicIndi" },
|
||||
"mother": {"$ref": "#/definitions/BasicIndi"},
|
||||
"father": {"$ref": "#/definitions/BasicIndi"},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/BasicIndi" }
|
||||
"items": {"$ref": "#/definitions/BasicIndi"}
|
||||
},
|
||||
"spouses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"spouse": { "$ref": "#/definitions/BasicIndi" },
|
||||
"spouse": {"$ref": "#/definitions/BasicIndi"},
|
||||
"marriage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": { "type": "string" },
|
||||
"place": { "type": "string" }
|
||||
"date": {"type": "string"},
|
||||
"place": {"type": "string"}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -123,187 +170,293 @@ To enable smooth AI communication, the following tools are exposed to the LLM. R
|
||||
```
|
||||
|
||||
### 1. `get_selected_person`
|
||||
Returns the individual currently selected in the browser viewport. This corresponds to the person displayed in the side panel, which is not necessarily the focused person.
|
||||
* **Request Schema:**
|
||||
|
||||
Returns the individual currently selected in the browser viewport. This
|
||||
corresponds to the person displayed in the side panel, which is not necessarily
|
||||
the focused person.
|
||||
|
||||
- **Request Schema:**
|
||||
```json
|
||||
{ "type": "object", "properties": {} }
|
||||
{"type": "object", "properties": {}}
|
||||
```
|
||||
* **Response Schema:** `FullIndi`
|
||||
- **Response Schema:** `FullIndi`
|
||||
|
||||
### 2. `search_indi`
|
||||
Searches the genealogy index for individuals by name. Returns at most 20 results to maintain fast performance and reasonable payload sizes.
|
||||
* **Request Schema:**
|
||||
|
||||
Searches the genealogy index for individuals by name. Returns at most 20 results
|
||||
to maintain fast performance and reasonable payload sizes.
|
||||
|
||||
- **Request Schema:**
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": { "type": "string", "description": "The name of the person to search for." }
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The name of the person to search for."
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
```
|
||||
* **Response Schema:** Array of `BasicIndi` (maximum 20 items).
|
||||
- **Response Schema:** Array of `BasicIndi` (maximum 20 items).
|
||||
|
||||
### 3. `inspect_indi`
|
||||
|
||||
Fetches isolated detailed information for a specific individual by ID pointer.
|
||||
* **Request Schema:**
|
||||
|
||||
- **Request Schema:**
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string", "description": "The pointer ID of the individual." }
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The pointer ID of the individual."
|
||||
}
|
||||
},
|
||||
"required": ["id"]
|
||||
}
|
||||
```
|
||||
* **Response Schema:** `FullIndi`
|
||||
- **Response Schema:** `FullIndi`
|
||||
|
||||
### 4. `focus_indi`
|
||||
Instructs the Topola viewer camera view to center on and focus a specific relative node. This will also update the side panel to show the focused person.
|
||||
|
||||
> [!NOTE]
|
||||
> Changing focus prompts a full redesign layout sweep to center that person. This creates high UX layout jitter if the assistant uses it repetitively.
|
||||
Instructs the Topola viewer camera view to center on and focus a specific
|
||||
relative node. This will also update the side panel to show the focused person.
|
||||
|
||||
* **Request Schema:**
|
||||
> [!NOTE] Changing focus prompts a full redesign layout sweep to center that
|
||||
> person. This creates high UX layout jitter if the assistant uses it
|
||||
> repetitively.
|
||||
|
||||
- **Request Schema:**
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string", "description": "The pointer ID to focus." }
|
||||
"id": {"type": "string", "description": "The pointer ID to focus."}
|
||||
},
|
||||
"required": ["id"]
|
||||
}
|
||||
```
|
||||
* **Response Schema:**
|
||||
- **Response Schema:**
|
||||
```json
|
||||
{ "type": "object", "properties": { "status": { "type": "string" } } }
|
||||
{"type": "object", "properties": {"status": {"type": "string"}}}
|
||||
```
|
||||
|
||||
### 5. `find_relationship_path`
|
||||
Traverses the internal graph model to find relative step paths between two individuals. The following links should be traversed: parent, child, spouse, sibling.
|
||||
* **Request Schema:**
|
||||
|
||||
Traverses the internal graph model to find relative step paths between two
|
||||
individuals. The following links should be traversed: parent, child, spouse,
|
||||
sibling.
|
||||
|
||||
- **Request Schema:**
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": { "type": "string", "description": "Start individual ID pointer" },
|
||||
"target": { "type": "string", "description": "End individual ID pointer" }
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "Start individual ID pointer"
|
||||
},
|
||||
"target": {"type": "string", "description": "End individual ID pointer"}
|
||||
},
|
||||
"required": ["source", "target"]
|
||||
}
|
||||
```
|
||||
* **Response Schema:** Array of `BasicIndi` establishing the sequence.
|
||||
- **Response Schema:** Array of `BasicIndi` establishing the sequence.
|
||||
|
||||
### 6. `get_ancestors`
|
||||
|
||||
Traverses upwards up to bounded ceiling generations.
|
||||
* **Request Schema:**
|
||||
|
||||
- **Request Schema:**
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string", "description": "Target individual ID" },
|
||||
"generations": { "type": "number", "description": "Depth bound" }
|
||||
"id": {"type": "string", "description": "Target individual ID"},
|
||||
"generations": {"type": "number", "description": "Depth bound"}
|
||||
},
|
||||
"required": ["id"]
|
||||
}
|
||||
```
|
||||
* **Response Schema:** Array of `BasicIndi`
|
||||
- **Response Schema:** Array of `BasicIndi`
|
||||
|
||||
### 7. `get_descendants`
|
||||
|
||||
Traverses downwards up to bounded ceiling generations.
|
||||
* **Request Schema:**
|
||||
|
||||
- **Request Schema:**
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string", "description": "Target individual ID" },
|
||||
"generations": { "type": "number", "description": "Depth bound" }
|
||||
"id": {"type": "string", "description": "Target individual ID"},
|
||||
"generations": {"type": "number", "description": "Depth bound"}
|
||||
},
|
||||
"required": ["id"]
|
||||
}
|
||||
```
|
||||
* **Response Schema:** Array of `BasicIndi`
|
||||
- **Response Schema:** Array of `BasicIndi`
|
||||
|
||||
## Constraints and Assumptions
|
||||
* **Family Structure Assumptions:** The WebMCP integration assumes simplified family structures (e.g., single set of biological parents per individual). Multiple marriages are fully supported, consistent with the core Topola Viewer app design.
|
||||
* **Privacy Boundaries:** The WebMCP tools must strictly follow the privacy constraints of the Topola Viewer. For instance, private profiles (such as WikiTree restricted profiles) must be filtered and hidden from the AI assistant and its tool responses.
|
||||
* **Cycle Protection:** All graph traversal algorithms (e.g., finding ancestors and descendants) must implement internal cycle protection (e.g., tracking visited node pointers) to avoid endless iteration loops caused by standard pedigree collapse.
|
||||
* **Legacy Replacement:** WebMCP tool creation must strictly overwrite and replace the existing tools previously defined in `src/webmcp.ts` to avoid duplicate hooks.
|
||||
* **Relationship Pathfinder:** Topola viewer does not have prebuilt pathfinder operators. `find_relationship_path` must be manually implemented from scratch using a standard Breadth-First Search (BFS) algorithm traversing parent, child, spouse, and sibling links.
|
||||
* **Index searching reuse:** The tool `search_indi` must borrow standard `buildSearchIndex` from `src/menu/search_index.ts` already powering the top search UI instead of deploying newly written independent fuzzy match loops.
|
||||
* **Data Format Standards:** WebMCP tool integration consumes Topola's pre-parsed core JSON formats (`JsonGedcomData`, `JsonIndi`) instead of low-level raw GEDCOM pointer lines to enforce implementation consistency and operational efficiency.
|
||||
|
||||
- **Family Structure Assumptions:** The WebMCP integration assumes simplified
|
||||
family structures (e.g., single set of biological parents per individual).
|
||||
Multiple marriages are fully supported, consistent with the core Topola Viewer
|
||||
app design.
|
||||
- **Privacy Boundaries:** The WebMCP tools must strictly follow the privacy
|
||||
constraints of the Topola Viewer. For instance, private profiles (such as
|
||||
WikiTree restricted profiles) must be filtered and hidden from the AI
|
||||
assistant and its tool responses.
|
||||
- **Cycle Protection:** All graph traversal algorithms (e.g., finding ancestors
|
||||
and descendants) must implement internal cycle protection (e.g., tracking
|
||||
visited node pointers) to avoid endless iteration loops caused by standard
|
||||
pedigree collapse.
|
||||
- **Legacy Replacement:** WebMCP tool creation must strictly overwrite and
|
||||
replace the existing tools previously defined in `src/webmcp.ts` to avoid
|
||||
duplicate hooks.
|
||||
- **Relationship Pathfinder:** Topola viewer does not have prebuilt pathfinder
|
||||
operators. `find_relationship_path` must be manually implemented from scratch
|
||||
using a standard Breadth-First Search (BFS) algorithm traversing parent,
|
||||
child, spouse, and sibling links.
|
||||
- **Index searching reuse:** The tool `search_indi` must borrow standard
|
||||
`buildSearchIndex` from `src/menu/search_index.ts` already powering the top
|
||||
search UI instead of deploying newly written independent fuzzy match loops.
|
||||
- **Data Format Standards:** WebMCP tool integration consumes Topola's
|
||||
pre-parsed core JSON formats (`JsonGedcomData`, `JsonIndi`) instead of
|
||||
low-level raw GEDCOM pointer lines to enforce implementation consistency and
|
||||
operational efficiency.
|
||||
|
||||
## Detailed Implementation Plan
|
||||
This section lists the exact files to be created or modified to execute this design successfully.
|
||||
|
||||
This section lists the exact files to be created or modified to execute this
|
||||
design successfully.
|
||||
|
||||
#### 1. [Modify] [webmcp.ts](../src/webmcp.ts)
|
||||
* **Rationale:** Serves as the core integration plug for the experimental WebMCP browser assistant setup and standard operational in-memory state cache.
|
||||
* **Action steps:**
|
||||
* Define isolated state stores for current `selection`, `detailIndi`, and `loadedGedcomData`.
|
||||
* Refactor current custom callbacks to register the complete tools collection blueprint (`search_indi`, `inspect_indi`, `focus_indi`, `get_ancestors`, `get_descendants`, `find_relationship_path`) into the `navigator.modelContext` array hook.
|
||||
* Expose default standard state setters for Topola view adapter.
|
||||
* Implement conversion and response helpers (`toMcpResponse`, `textMcpResponse`, `toBasicIndi`, `toFullIndi`) to standardise in-transit JSON streams.
|
||||
|
||||
- **Rationale:** Serves as the core integration plug for the experimental WebMCP
|
||||
browser assistant setup and standard operational in-memory state cache.
|
||||
- **Action steps:**
|
||||
- Define isolated state stores for current `selection`, `detailIndi`, and
|
||||
`loadedGedcomData`.
|
||||
- Refactor current custom callbacks to register the complete tools collection
|
||||
blueprint (`search_indi`, `inspect_indi`, `focus_indi`, `get_ancestors`,
|
||||
`get_descendants`, `find_relationship_path`) into the
|
||||
`navigator.modelContext` array hook.
|
||||
- Expose default standard state setters for Topola view adapter.
|
||||
- Implement conversion and response helpers (`toMcpResponse`,
|
||||
`textMcpResponse`, `toBasicIndi`, `toFullIndi`) to standardise in-transit
|
||||
JSON streams.
|
||||
|
||||
### 2. [NEW] [webmcp_definitions.ts](../src/webmcp_definitions.ts)
|
||||
* **Rationale:** Keeps standard LLM tool definition blueprints separate from the execution bridge to avoid bloat and single interface monolithic designs.
|
||||
|
||||
- **Rationale:** Keeps standard LLM tool definition blueprints separate from the
|
||||
execution bridge to avoid bloat and single interface monolithic designs.
|
||||
|
||||
### 3. [NEW] [webmcp_types.ts](../src/webmcp_types.ts)
|
||||
* **Rationale:** Defines ambient `navigator.modelContext` parameters and concrete structural bridge types cleanly.
|
||||
|
||||
- **Rationale:** Defines ambient `navigator.modelContext` parameters and
|
||||
concrete structural bridge types cleanly.
|
||||
|
||||
### 4. [Modify] [app.tsx](../src/app.tsx)
|
||||
* **Rationale:** The top-level state component for Topola Viewer. It holds the interactive chart state and needs standard side effect hooks to update the WebMCP context on active selections.
|
||||
* **Action steps:**
|
||||
* Initialize WebMCP Bridge securely using `useState(() => new WebMcpBridge())` avoiding loose disconnected singleton memory leaks.
|
||||
* Add standard `React.useEffect` hook to monitor active viewport selection changes and feed them into the WebMCP in-transit state.
|
||||
* Expose selection and inspection callbacks handlers to the bridge hook preset.
|
||||
|
||||
- **Rationale:** The top-level state component for Topola Viewer. It holds the
|
||||
interactive chart state and needs standard side effect hooks to update the
|
||||
WebMCP context on active selections.
|
||||
- **Action steps:**
|
||||
- Initialize WebMCP Bridge securely using `useState(() => new WebMcpBridge())`
|
||||
avoiding loose disconnected singleton memory leaks.
|
||||
- Add standard `React.useEffect` hook to monitor active viewport selection
|
||||
changes and feed them into the WebMCP in-transit state.
|
||||
- Expose selection and inspection callbacks handlers to the bridge hook
|
||||
preset.
|
||||
|
||||
### 5. [Modify] [gedcom_util.ts](../src/util/gedcom_util.ts)
|
||||
* **Rationale:** Handles core conversion formulas from raw gedcom pointers to JSON objects. Houses newly proposed BFS algorithms avoiding visual rendering components dependency.
|
||||
* **Action steps:**
|
||||
* Implement standard Breadth-First Search (BFS) method for isolated `find_relationship_path` relative footprint.
|
||||
* Draft flat array collection algorithms (bounded up to preset depth ceiling) for ancestors and descendants generation list.
|
||||
|
||||
- **Rationale:** Handles core conversion formulas from raw gedcom pointers to
|
||||
JSON objects. Houses newly proposed BFS algorithms avoiding visual rendering
|
||||
components dependency.
|
||||
- **Action steps:**
|
||||
- Implement standard Breadth-First Search (BFS) method for isolated
|
||||
`find_relationship_path` relative footprint.
|
||||
- Draft flat array collection algorithms (bounded up to preset depth ceiling)
|
||||
for ancestors and descendants generation list.
|
||||
|
||||
### 6. [Modify] [gedcom_util.spec.ts](../src/util/gedcom_util.spec.ts)
|
||||
* **Rationale:** Standard isolated unit test suite. It must accommodate boundary tests for newly added generic algorithms.
|
||||
* **Action steps:**
|
||||
* Add unit test cases for `find_relationship_path` with disconnected and connected multi relationships.
|
||||
* Add test vectors for `get_ancestors` boundary ceilings (e.g., 5 generations) and cycles control.
|
||||
|
||||
- **Rationale:** Standard isolated unit test suite. It must accommodate boundary
|
||||
tests for newly added generic algorithms.
|
||||
- **Action steps:**
|
||||
- Add unit test cases for `find_relationship_path` with disconnected and
|
||||
connected multi relationships.
|
||||
- Add test vectors for `get_ancestors` boundary ceilings (e.g., 5 generations)
|
||||
and cycles control.
|
||||
|
||||
### 7. [New] [webmcp.cy.js](../cypress/e2e/webmcp.cy.js)
|
||||
* **Rationale:** Formatted test files acting as automated integration coverage. Leverages Cypress stubs for isolated web tools inspection.
|
||||
* **Action steps:**
|
||||
* Mock `navigator.modelContext` using `cy.visit` on before preset lifecycle hooks.
|
||||
* Trigger tool actions and check default DOM element shifts in simulated Topola frames.
|
||||
|
||||
- **Rationale:** Formatted test files acting as automated integration coverage.
|
||||
Leverages Cypress stubs for isolated web tools inspection.
|
||||
- **Action steps:**
|
||||
- Mock `navigator.modelContext` using `cy.visit` on before preset lifecycle
|
||||
hooks.
|
||||
- Trigger tool actions and check default DOM element shifts in simulated
|
||||
Topola frames.
|
||||
|
||||
## Testing Strategy
|
||||
To ensure the robustness and correctness of the WebMCP integration, we will employ a multi-tiered testing approach spanning unit, integration, and manual end-to-end tests.
|
||||
|
||||
To ensure the robustness and correctness of the WebMCP integration, we will
|
||||
employ a multi-tiered testing approach spanning unit, integration, and manual
|
||||
end-to-end tests.
|
||||
|
||||
### Unit Tests
|
||||
* **Graph Traversal Algorithms:**
|
||||
* Test `find_relationship_path` with multiple scenarios:
|
||||
* Direct descendants (e.g., Parent to Child).
|
||||
* Sibling and cousin relationships.
|
||||
* Pedigree collapse (cycles in the family tree).
|
||||
* Unrelated individuals (should return an empty list or appropriate error).
|
||||
* Test `get_ancestors` and `get_descendants` with generation bounds (e.g., limit = 5) and deep pedigree setups to verify the boundary ceilings and internal cycle protection.
|
||||
* **Search indexing:** Test `search_indi` to verify it delegates correctly to the core search index and correctly limits the size of the response payload to at most 20 items.
|
||||
|
||||
- **Graph Traversal Algorithms:**
|
||||
- Test `find_relationship_path` with multiple scenarios:
|
||||
- Direct descendants (e.g., Parent to Child).
|
||||
- Sibling and cousin relationships.
|
||||
- Pedigree collapse (cycles in the family tree).
|
||||
- Unrelated individuals (should return an empty list or appropriate error).
|
||||
- Test `get_ancestors` and `get_descendants` with generation bounds (e.g.,
|
||||
limit = 5) and deep pedigree setups to verify the boundary ceilings and
|
||||
internal cycle protection.
|
||||
- **Search indexing:** Test `search_indi` to verify it delegates correctly to
|
||||
the core search index and correctly limits the size of the response payload to
|
||||
at most 20 items.
|
||||
|
||||
### Integration Tests
|
||||
* Standard UI integration tests are implemented using **Cypress**.
|
||||
* The `navigator.modelContext` can be stubbed using Cypress `onBeforeLoad` hook to verify standard registration callbacks on application setup frame.
|
||||
* The integration suite tests:
|
||||
* Core tool callbacks correctly transition the React internal viewport selection.
|
||||
* Application changes in selected person correctly propagate into in-transit operational state without rendering glitches.
|
||||
|
||||
- Standard UI integration tests are implemented using **Cypress**.
|
||||
- The `navigator.modelContext` can be stubbed using Cypress `onBeforeLoad` hook
|
||||
to verify standard registration callbacks on application setup frame.
|
||||
- The integration suite tests:
|
||||
- Core tool callbacks correctly transition the React internal viewport
|
||||
selection.
|
||||
- Application changes in selected person correctly propagate into in-transit
|
||||
operational state without rendering glitches.
|
||||
|
||||
### Manual Verification
|
||||
* Because interactive tools are bound to the experimental WebMCP protocol, manual verification can be accelerated using the **Model Context Tool Inspector Chrome Extension**. This grants operational developers a dashboard panel to trigger and fire tools independently inside standard dev viewports.
|
||||
|
||||
- Because interactive tools are bound to the experimental WebMCP protocol,
|
||||
manual verification can be accelerated using the **Model Context Tool
|
||||
Inspector Chrome Extension**. This grants operational developers a dashboard
|
||||
panel to trigger and fire tools independently inside standard dev viewports.
|
||||
|
||||
### Files Created or Modified for Testing
|
||||
* **[Modify] [gedcom_util.spec.ts](../src/util/gedcom_util.spec.ts)**
|
||||
* **Rationale:** Contains existing unit tests for GEDCOM data structures. It will be extended to verify the newly introduced relationship finding and bounded graph traversal algorithms without visual overhead.
|
||||
* **[New] [webmcp.cy.js](../cypress/e2e/webmcp.cy.js)**
|
||||
* **Rationale:** Will act as the dedicated automated integration suite for the WebMCP feature. It will stub `navigator.modelContext` to verify correct tool registration and that standard execution callbacks successfully sync back layout and selection changes inside the Topola visual DOM.
|
||||
|
||||
- **[Modify] [gedcom_util.spec.ts](../src/util/gedcom_util.spec.ts)**
|
||||
- **Rationale:** Contains existing unit tests for GEDCOM data structures. It
|
||||
will be extended to verify the newly introduced relationship finding and
|
||||
bounded graph traversal algorithms without visual overhead.
|
||||
- **[New] [webmcp.cy.js](../cypress/e2e/webmcp.cy.js)**
|
||||
- **Rationale:** Will act as the dedicated automated integration suite for the
|
||||
WebMCP feature. It will stub `navigator.modelContext` to verify correct tool
|
||||
registration and that standard execution callbacks successfully sync back
|
||||
layout and selection changes inside the Topola visual DOM.
|
||||
|
||||
## Future Considerations
|
||||
* **AI Canvas & Camera controls:** Exposing interactive UI commands such as canvas zoom and shifting the chart views (e.g., hourglass, donatso) could be added in future increments as additional tool blueprints.
|
||||
|
||||
- **AI Canvas & Camera controls:** Exposing interactive UI commands such as
|
||||
canvas zoom and shifting the chart views (e.g., hourglass, donatso) could be
|
||||
added in future increments as additional tool blueprints.
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"test": "jest",
|
||||
"prettier": "prettier --write \"{src,tests}/**/*.{ts,tsx,json}\"",
|
||||
"prettier": "prettier --write \"{src,tests,docs}/**/*.{ts,tsx,json,md}\" \"*.md\"",
|
||||
"lint": "eslint src tests --ext .ts,.tsx",
|
||||
"predeploy": "npm run build",
|
||||
"deploy": "gh-pages -d dist",
|
||||
|
||||
@@ -2,7 +2,16 @@ const config = {
|
||||
bracketSpacing: false,
|
||||
endOfLine: 'lf',
|
||||
singleQuote: true,
|
||||
printWidth: 80,
|
||||
plugins: ['prettier-plugin-organize-imports'],
|
||||
overrides: [
|
||||
{
|
||||
files: '*.md',
|
||||
options: {
|
||||
proseWrap: 'always',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,39 +1,65 @@
|
||||
# Source Directory (`src`)
|
||||
|
||||
This directory contains the main source code for the Topola Viewer application. It includes the application entry point, main layout components, chart rendering logic, data source adapters, menus, side panel, translations, and utilities.
|
||||
This directory contains the main source code for the Topola Viewer application.
|
||||
It includes the application entry point, main layout components, chart rendering
|
||||
logic, data source adapters, menus, side panel, translations, and utilities.
|
||||
|
||||
## Subdirectories
|
||||
|
||||
* **[datasource](datasource)**: Implements data sources for loading GEDCOM files, URLs, WikiTree API data, and embedded data.
|
||||
* **[menu](menu)**: Contains components for the top navigation bar, search bar, and various menus (upload, URL, WikiTree).
|
||||
* **[sidepanel](sidepanel)**: Contains components for the side panel, which shows individual details and chart configuration settings.
|
||||
* **[translations](translations)**: Contains JSON files with translations for supported languages.
|
||||
* **[util](util)**: Contains utility functions for dates, GEDCOM data, analytics, and responsive design.
|
||||
- **[datasource](datasource)**: Implements data sources for loading GEDCOM
|
||||
files, URLs, WikiTree API data, and embedded data.
|
||||
- **[menu](menu)**: Contains components for the top navigation bar, search bar,
|
||||
and various menus (upload, URL, WikiTree).
|
||||
- **[sidepanel](sidepanel)**: Contains components for the side panel, which
|
||||
shows individual details and chart configuration settings.
|
||||
- **[translations](translations)**: Contains JSON files with translations for
|
||||
supported languages.
|
||||
- **[util](util)**: Contains utility functions for dates, GEDCOM data,
|
||||
analytics, and responsive design.
|
||||
|
||||
## Files
|
||||
|
||||
* **[app.tsx](app.tsx)**: The main application component that manages state, routing, data loading orchestration, and high-level layout.
|
||||
* **[changelog.tsx](changelog.tsx)**: Component that fetches and displays the recent changes in a modal dialog when the app is updated.
|
||||
* **[chart.tsx](chart.tsx)**: The primary chart rendering component using D3 and the `topola` library. It handles zoom, pan, and export actions.
|
||||
* **[donatso-chart.tsx](donatso-chart.tsx)**: An alternative chart view utilizing the `family-chart` library.
|
||||
* **[index.tsx](index.tsx)**: The entry point of the application. It sets up polyfills, internationalization, and renders the app root.
|
||||
* **[intro.tsx](intro.tsx)**: The landing page component shown when no data is loaded. It provides instructions, examples, and privacy information.
|
||||
* **[webmcp.ts](webmcp.ts)**: Model Context Protocol (MCP) implementation to expose viewer state and action handlers for dynamic AI model interactions and tool definitions.
|
||||
* **[webmcp_definitions.ts](webmcp_definitions.ts)**: Defines custom schemas and descriptions for various registration tools (such as person search, ancestry detail fetch, navigation triggers, etc.) exposed through the WebMCP protocol.
|
||||
* **[webmcp_types.ts](webmcp_types.ts)**: Common TypeScript type annotations and structures used within the WebMCP engine layer.
|
||||
- **[app.tsx](app.tsx)**: The main application component that manages state,
|
||||
routing, data loading orchestration, and high-level layout.
|
||||
- **[changelog.tsx](changelog.tsx)**: Component that fetches and displays the
|
||||
recent changes in a modal dialog when the app is updated.
|
||||
- **[chart.tsx](chart.tsx)**: The primary chart rendering component using D3 and
|
||||
the `topola` library. It handles zoom, pan, and export actions.
|
||||
- **[donatso-chart.tsx](donatso-chart.tsx)**: An alternative chart view
|
||||
utilizing the `family-chart` library.
|
||||
- **[index.tsx](index.tsx)**: The entry point of the application. It sets up
|
||||
polyfills, internationalization, and renders the app root.
|
||||
- **[intro.tsx](intro.tsx)**: The landing page component shown when no data is
|
||||
loaded. It provides instructions, examples, and privacy information.
|
||||
- **[webmcp.ts](webmcp.ts)**: Model Context Protocol (MCP) implementation to
|
||||
expose viewer state and action handlers for dynamic AI model interactions and
|
||||
tool definitions.
|
||||
- **[webmcp_definitions.ts](webmcp_definitions.ts)**: Defines custom schemas and
|
||||
descriptions for various registration tools (such as person search, ancestry
|
||||
detail fetch, navigation triggers, etc.) exposed through the WebMCP protocol.
|
||||
- **[webmcp_types.ts](webmcp_types.ts)**: Common TypeScript type annotations and
|
||||
structures used within the WebMCP engine layer.
|
||||
|
||||
## Assets
|
||||
|
||||
* **[index.css](index.css)**: Global CSS styles for the application.
|
||||
* **[topola.jpg](topola.jpg)**: Image asset for the Topola logo.
|
||||
- **[index.css](index.css)**: Global CSS styles for the application.
|
||||
- **[topola.jpg](topola.jpg)**: Image asset for the Topola logo.
|
||||
|
||||
## Type Definitions
|
||||
|
||||
* **[family-chart.d.ts](family-chart.d.ts)**: Type definitions for the `family-chart` library.
|
||||
* **[imports.d.ts](imports.d.ts)**: Type definitions for non-code assets (e.g., CSS imports).
|
||||
* **[javascript-natural-sort.d.ts](javascript-natural-sort.d.ts)**: Type definitions for the `javascript-natural-sort` library.
|
||||
* **[lunr-languages.d.ts](lunr-languages.d.ts)**: Type definitions for `lunr-languages` plugins.
|
||||
* **[parse-gedcom.d.ts](parse-gedcom.d.ts)**: Type definitions for the `parse-gedcom` library.
|
||||
* **[react-app-env.d.ts](react-app-env.d.ts)**: Type definitions for React application environment.
|
||||
* **[react-linkify.d.ts](react-linkify.d.ts)**: Type definitions for the `react-linkify` library.
|
||||
* **[vite-env.d.ts](vite-env.d.ts)**: Type definitions for Vite environment variables.
|
||||
- **[family-chart.d.ts](family-chart.d.ts)**: Type definitions for the
|
||||
`family-chart` library.
|
||||
- **[imports.d.ts](imports.d.ts)**: Type definitions for non-code assets (e.g.,
|
||||
CSS imports).
|
||||
- **[javascript-natural-sort.d.ts](javascript-natural-sort.d.ts)**: Type
|
||||
definitions for the `javascript-natural-sort` library.
|
||||
- **[lunr-languages.d.ts](lunr-languages.d.ts)**: Type definitions for
|
||||
`lunr-languages` plugins.
|
||||
- **[parse-gedcom.d.ts](parse-gedcom.d.ts)**: Type definitions for the
|
||||
`parse-gedcom` library.
|
||||
- **[react-app-env.d.ts](react-app-env.d.ts)**: Type definitions for React
|
||||
application environment.
|
||||
- **[react-linkify.d.ts](react-linkify.d.ts)**: Type definitions for the
|
||||
`react-linkify` library.
|
||||
- **[vite-env.d.ts](vite-env.d.ts)**: Type definitions for Vite environment
|
||||
variables.
|
||||
|
||||
@@ -1,19 +1,40 @@
|
||||
# Data Sources
|
||||
|
||||
This directory contains the implementation of various data sources supported by Topola Viewer. It handles loading genealogical data from different origins and transforming it into a unified format used by the application.
|
||||
This directory contains the implementation of various data sources supported by
|
||||
Topola Viewer. It handles loading genealogical data from different origins and
|
||||
transforming it into a unified format used by the application.
|
||||
|
||||
## Purpose
|
||||
|
||||
The main purpose of this directory is to abstract away the details of where the genealogical data comes from. Whether it is a file uploaded by the user, a URL pointing to a GEDCOM file, data fetched from the WikiTree API, or data received from a parent window in embedded mode, this directory provides a consistent interface ([DataSource](data_source.ts#L19)) to the rest of the application.
|
||||
The main purpose of this directory is to abstract away the details of where the
|
||||
genealogical data comes from. Whether it is a file uploaded by the user, a URL
|
||||
pointing to a GEDCOM file, data fetched from the WikiTree API, or data received
|
||||
from a parent window in embedded mode, this directory provides a consistent
|
||||
interface ([DataSource](data_source.ts#L19)) to the rest of the application.
|
||||
|
||||
## Files
|
||||
|
||||
- [data_source.ts](data_source.ts): Defines the core abstractions for data sources, including the [DataSourceEnum](data_source.ts#L5) and the [DataSource](data_source.ts#L19) interface.
|
||||
- [load_data.ts](load_data.ts): Contains helper functions for loading data from files and URLs, handling zip files (GEDZIP), and implements [UploadedDataSource](load_data.ts#L155) and [GedcomUrlDataSource](load_data.ts#L195).
|
||||
- [embedded.ts](embedded.ts): Implements [EmbeddedDataSource](embedded.ts#L34) for cases where the viewer is embedded in an iframe and receives GEDCOM data via window messages.
|
||||
- [gedcom_generator.ts](gedcom_generator.ts): Used to create a GEDCOM structure from internal JSON data. This is primarily used to generate a GEDCOM representation for data sources that do not natively provide it (like WikiTree), which is needed for the details panel.
|
||||
- [wikitree.ts](wikitree.ts): Implements [WikiTreeDataSource](wikitree.ts#L62) and serves as the main entry point for loading data from WikiTree.
|
||||
- [wikitree_api.ts](wikitree_api.ts): Handles the actual communication with the WikiTree API using the `wikitree-js` library, including caching and handling private profiles.
|
||||
- [wikitree_transformer.ts](wikitree_transformer.ts): Transforms data fetched from the WikiTree API into Topola's internal data format.
|
||||
- [load_data.spec.ts](load_data.spec.ts): Unit tests for the data loading functions in `load_data.ts`.
|
||||
- [data_source.ts](data_source.ts): Defines the core abstractions for data
|
||||
sources, including the [DataSourceEnum](data_source.ts#L5) and the
|
||||
[DataSource](data_source.ts#L19) interface.
|
||||
- [load_data.ts](load_data.ts): Contains helper functions for loading data from
|
||||
files and URLs, handling zip files (GEDZIP), and implements
|
||||
[UploadedDataSource](load_data.ts#L155) and
|
||||
[GedcomUrlDataSource](load_data.ts#L195).
|
||||
- [embedded.ts](embedded.ts): Implements [EmbeddedDataSource](embedded.ts#L34)
|
||||
for cases where the viewer is embedded in an iframe and receives GEDCOM data
|
||||
via window messages.
|
||||
- [gedcom_generator.ts](gedcom_generator.ts): Used to create a GEDCOM structure
|
||||
from internal JSON data. This is primarily used to generate a GEDCOM
|
||||
representation for data sources that do not natively provide it (like
|
||||
WikiTree), which is needed for the details panel.
|
||||
- [wikitree.ts](wikitree.ts): Implements [WikiTreeDataSource](wikitree.ts#L62)
|
||||
and serves as the main entry point for loading data from WikiTree.
|
||||
- [wikitree_api.ts](wikitree_api.ts): Handles the actual communication with the
|
||||
WikiTree API using the `wikitree-js` library, including caching and handling
|
||||
private profiles.
|
||||
- [wikitree_transformer.ts](wikitree_transformer.ts): Transforms data fetched
|
||||
from the WikiTree API into Topola's internal data format.
|
||||
- [load_data.spec.ts](load_data.spec.ts): Unit tests for the data loading
|
||||
functions in `load_data.ts`.
|
||||
- `testdata/`: A directory containing test files used by the unit tests.
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
# Menu Directory
|
||||
|
||||
This directory contains React components that make up the top navigation bar and its associated menus and dialogs in Topola Viewer. It handles loading data from various sources, searching for individuals, and changing chart views.
|
||||
This directory contains React components that make up the top navigation bar and
|
||||
its associated menus and dialogs in Topola Viewer. It handles loading data from
|
||||
various sources, searching for individuals, and changing chart views.
|
||||
|
||||
## Files
|
||||
|
||||
- [menu_item.tsx](menu_item.tsx): A utility component that renders as either a `Menu.Item` or a `Dropdown.Item` from `semantic-ui-react`, allowing menu items to be shared between desktop and mobile layouts.
|
||||
- [search.tsx](search.tsx): The `SearchBar` component displayed in the top bar. It handles user input and displays search results.
|
||||
- [search_index.ts](search_index.ts): Implements the search index using the `lunr` library. It creates a searchable index of individuals from the loaded genealogy data, supporting multilingual search and character normalization.
|
||||
- [top_bar.tsx](top_bar.tsx): The main component for the application's top navigation bar. It handles responsive design for small and large screens and aggregates all the menus and the search bar.
|
||||
- [upload_menu.tsx](upload_menu.tsx): The "Open file" menu item. It allows users to upload local GEDCOM files and images.
|
||||
- [url_menu.tsx](url_menu.tsx): The "Load from URL" menu item and modal dialog. It allows users to load data from a URL, using a proxy to avoid CORS issues.
|
||||
- [wikitree_menu.tsx](wikitree_menu.tsx): Menu items and modal dialogs for WikiTree integration, including loading by WikiTree ID and logging in.
|
||||
- [wikitree.png](wikitree.png): Image asset for the WikiTree logo used in the menus.
|
||||
- [menu_item.tsx](menu_item.tsx): A utility component that renders as either a
|
||||
`Menu.Item` or a `Dropdown.Item` from `semantic-ui-react`, allowing menu items
|
||||
to be shared between desktop and mobile layouts.
|
||||
- [search.tsx](search.tsx): The `SearchBar` component displayed in the top bar.
|
||||
It handles user input and displays search results.
|
||||
- [search_index.ts](search_index.ts): Implements the search index using the
|
||||
`lunr` library. It creates a searchable index of individuals from the loaded
|
||||
genealogy data, supporting multilingual search and character normalization.
|
||||
- [top_bar.tsx](top_bar.tsx): The main component for the application's top
|
||||
navigation bar. It handles responsive design for small and large screens and
|
||||
aggregates all the menus and the search bar.
|
||||
- [upload_menu.tsx](upload_menu.tsx): The "Open file" menu item. It allows users
|
||||
to upload local GEDCOM files and images.
|
||||
- [url_menu.tsx](url_menu.tsx): The "Load from URL" menu item and modal dialog.
|
||||
It allows users to load data from a URL, using a proxy to avoid CORS issues.
|
||||
- [wikitree_menu.tsx](wikitree_menu.tsx): Menu items and modal dialogs for
|
||||
WikiTree integration, including loading by WikiTree ID and logging in.
|
||||
- [wikitree.png](wikitree.png): Image asset for the WikiTree logo used in the
|
||||
menus.
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
# Side Panel Directory
|
||||
|
||||
This directory contains components and logic for the side panel of the Topola Viewer application. The side panel is used to display detailed information about a selected individual and to configure chart settings.
|
||||
This directory contains components and logic for the side panel of the Topola
|
||||
Viewer application. The side panel is used to display detailed information about
|
||||
a selected individual and to configure chart settings.
|
||||
|
||||
## Purpose
|
||||
|
||||
The purpose of this directory is to provide a collapsible side panel that offers:
|
||||
1. **Detailed Information**: Displays comprehensive data about a selected person, including their name, images, events, facts, notes, and sources, extracted from the loaded GEDCOM file.
|
||||
2. **Chart Configuration**: Provides controls for users to customize the appearance of the genealogical chart (e.g., color schemes, showing/hiding IDs, and sex indicators).
|
||||
The purpose of this directory is to provide a collapsible side panel that
|
||||
offers:
|
||||
|
||||
This directory acts as a container for these features, organizing them into specific subdirectories:
|
||||
- **[config](config/)**: Contains the UI and logic for managing chart display settings.
|
||||
- **[details](details/)**: Contains components for rendering the detailed view of an individual's data.
|
||||
- **[head](head/)**: Contains components to display metadata from the GEDCOM file header.
|
||||
1. **Detailed Information**: Displays comprehensive data about a selected
|
||||
person, including their name, images, events, facts, notes, and sources,
|
||||
extracted from the loaded GEDCOM file.
|
||||
2. **Chart Configuration**: Provides controls for users to customize the
|
||||
appearance of the genealogical chart (e.g., color schemes, showing/hiding
|
||||
IDs, and sex indicators).
|
||||
|
||||
This directory acts as a container for these features, organizing them into
|
||||
specific subdirectories:
|
||||
|
||||
- **[config](config/)**: Contains the UI and logic for managing chart display
|
||||
settings.
|
||||
- **[details](details/)**: Contains components for rendering the detailed view
|
||||
of an individual's data.
|
||||
- **[head](head/)**: Contains components to display metadata from the GEDCOM
|
||||
file header.
|
||||
|
||||
## Files in this Directory
|
||||
|
||||
- [side-panel.tsx](side-panel.tsx): The main React component that implements the side panel. It manages the layout, switching between expanded and collapsed states. When expanded, it displays a tabbed interface with "Info" (details) and "Settings" (configuration) panes. When collapsed in a mobile view, it shows a minimal view of the selected person's name. It also includes the button to toggle the panel's expanded state.
|
||||
- [side-panel.tsx](side-panel.tsx): The main React component that implements the
|
||||
side panel. It manages the layout, switching between expanded and collapsed
|
||||
states. When expanded, it displays a tabbed interface with "Info" (details)
|
||||
and "Settings" (configuration) panes. When collapsed in a mobile view, it
|
||||
shows a minimal view of the selected person's name. It also includes the
|
||||
button to toggle the panel's expanded state.
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
# Chart Configuration
|
||||
|
||||
This directory contains the logic and UI for configuring the display of the genealogical chart.
|
||||
This directory contains the logic and UI for configuring the display of the
|
||||
genealogical chart.
|
||||
|
||||
## Purpose
|
||||
|
||||
The purpose of this directory is to manage the user-configurable settings for the chart visualization. This includes:
|
||||
- **Colors**: How individuals in the chart are colored (none, by generation, or by sex).
|
||||
The purpose of this directory is to manage the user-configurable settings for
|
||||
the chart visualization. This includes:
|
||||
|
||||
- **Colors**: How individuals in the chart are colored (none, by generation, or
|
||||
by sex).
|
||||
- **IDs**: Whether to show or hide person IDs.
|
||||
- **Sex**: Whether to show or hide indicators for sex.
|
||||
|
||||
It provides the data structures for the configuration, default values, functions to serialize/deserialize the configuration to/from URL query parameters, and the UI component for the side panel.
|
||||
It provides the data structures for the configuration, default values, functions
|
||||
to serialize/deserialize the configuration to/from URL query parameters, and the
|
||||
UI component for the side panel.
|
||||
|
||||
## Files
|
||||
|
||||
- [config.tsx](config.tsx): The main file containing:
|
||||
- `ChartColors`, `Ids`, and `Sex` enums defining the available options.
|
||||
- `Config` interface defining the configuration state.
|
||||
- `argsToConfig` and `configToArgs` functions for mapping the configuration to and from URL query arguments.
|
||||
- `ConfigPanel` React component providing the UI controls (radio buttons) for these settings in the side panel.
|
||||
- `argsToConfig` and `configToArgs` functions for mapping the configuration to
|
||||
and from URL query arguments.
|
||||
- `ConfigPanel` React component providing the UI controls (radio buttons) for
|
||||
these settings in the side panel.
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
# Details Directory
|
||||
|
||||
This directory contains React components for displaying detailed information about a selected individual (person) in the side panel of the Topola Viewer application. The details are extracted from a parsed GEDCOM file.
|
||||
This directory contains React components for displaying detailed information
|
||||
about a selected individual (person) in the side panel of the Topola Viewer
|
||||
application. The details are extracted from a parsed GEDCOM file.
|
||||
|
||||
## Files
|
||||
|
||||
- [additional-files.tsx](additional-files.tsx): Displays a list of links to non-image files associated with an entry.
|
||||
- [collapsed-details.tsx](collapsed-details.tsx): Displays a minimal, vertical view of details (person's name) when the side panel is collapsed.
|
||||
- [details.tsx](details.tsx): The main component that orchestrates the display of all details for an individual, including name, images, events, facts, notes, and sources.
|
||||
- [event-extras.tsx](event-extras.tsx): Displays additional content for events (images, notes, sources, files) in a tabbed interface.
|
||||
- [events.tsx](events.tsx): Handles the logic and display for all events related to an individual, sorting them by date and grouping them by life stages.
|
||||
- [immediate-family.tsx](immediate-family.tsx): A dedicated side panel module for grouping and displaying parents, spouses, and children as rapid-navigation links.
|
||||
- [linkify-new-tab.tsx](linkify-new-tab.tsx): A helper component that wraps content and makes URLs clickable, opening them in a new tab.
|
||||
- [multiline-text.tsx](multiline-text.tsx): Helper component to display multi-line text with linkified URLs.
|
||||
- [person-link.tsx](person-link.tsx): Component that renders a clickable link to navigate to a person's profile view.
|
||||
- [additional-files.tsx](additional-files.tsx): Displays a list of links to
|
||||
non-image files associated with an entry.
|
||||
- [collapsed-details.tsx](collapsed-details.tsx): Displays a minimal, vertical
|
||||
view of details (person's name) when the side panel is collapsed.
|
||||
- [details.tsx](details.tsx): The main component that orchestrates the display
|
||||
of all details for an individual, including name, images, events, facts,
|
||||
notes, and sources.
|
||||
- [event-extras.tsx](event-extras.tsx): Displays additional content for events
|
||||
(images, notes, sources, files) in a tabbed interface.
|
||||
- [events.tsx](events.tsx): Handles the logic and display for all events related
|
||||
to an individual, sorting them by date and grouping them by life stages.
|
||||
- [immediate-family.tsx](immediate-family.tsx): A dedicated side panel module
|
||||
for grouping and displaying parents, spouses, and children as rapid-navigation
|
||||
links.
|
||||
- [linkify-new-tab.tsx](linkify-new-tab.tsx): A helper component that wraps
|
||||
content and makes URLs clickable, opening them in a new tab.
|
||||
- [multiline-text.tsx](multiline-text.tsx): Helper component to display
|
||||
multi-line text with linkified URLs.
|
||||
- [person-link.tsx](person-link.tsx): Component that renders a clickable link to
|
||||
navigate to a person's profile view.
|
||||
- [sources.tsx](sources.tsx): Displays a list of sources cited for an entry.
|
||||
- [translated-tag.tsx](translated-tag.tsx): Component to translate GEDCOM tags into human-readable labels.
|
||||
- [wrapped-image.tsx](wrapped-image.tsx): Component to display images with loading placeholders, error fallback, and a click-to-enlarge modal.
|
||||
- [translated-tag.tsx](translated-tag.tsx): Component to translate GEDCOM tags
|
||||
into human-readable labels.
|
||||
- [wrapped-image.tsx](wrapped-image.tsx): Component to display images with
|
||||
loading placeholders, error fallback, and a click-to-enlarge modal.
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
# Head Directory
|
||||
|
||||
This directory contains components responsible for displaying information from the header of the loaded GEDCOM file in the side panel.
|
||||
This directory contains components responsible for displaying information from
|
||||
the header of the loaded GEDCOM file in the side panel.
|
||||
|
||||
## Files
|
||||
|
||||
### [head.tsx](head.tsx)
|
||||
|
||||
Defines the `SourceHead` component. This component displays metadata about the data source, extracted from the GEDCOM header (`HEAD` record). It includes information such as:
|
||||
Defines the `SourceHead` component. This component displays metadata about the
|
||||
data source, extracted from the GEDCOM header (`HEAD` record). It includes
|
||||
information such as:
|
||||
|
||||
- The software name that generated the file (`SOUR`)
|
||||
- File creation date (`DATE`)
|
||||
- Original file name (`FILE`)
|
||||
- Submitter's name and contact details (phone, email, address) (`SUBM`)
|
||||
- Copyright information (`COPR`)
|
||||
|
||||
The component conditionally renders these details only if they are present in the data.
|
||||
The component conditionally renders these details only if they are present in
|
||||
the data.
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
# Translations
|
||||
|
||||
This directory contains translation files for the Topola Viewer application. These files are used to support multiple languages in the user interface.
|
||||
This directory contains translation files for the Topola Viewer application.
|
||||
These files are used to support multiple languages in the user interface.
|
||||
|
||||
## Files
|
||||
|
||||
Each file in this directory corresponds to a specific language and contains a JSON object with translation keys and their corresponding localized strings.
|
||||
Each file in this directory corresponds to a specific language and contains a
|
||||
JSON object with translation keys and their corresponding localized strings.
|
||||
|
||||
* [bg.json](bg.json): Bulgarian translations.
|
||||
* [cs.json](cs.json): Czech translations.
|
||||
* [de.json](de.json): German translations.
|
||||
* [fr.json](fr.json): French translations.
|
||||
* [it.json](it.json): Italian translations.
|
||||
* [pl.json](pl.json): Polish translations.
|
||||
* [ru.json](ru.json): Russian translations.
|
||||
* [sv.json](sv.json): Swedish translations.
|
||||
- [bg.json](bg.json): Bulgarian translations.
|
||||
- [cs.json](cs.json): Czech translations.
|
||||
- [de.json](de.json): German translations.
|
||||
- [fr.json](fr.json): French translations.
|
||||
- [it.json](it.json): Italian translations.
|
||||
- [pl.json](pl.json): Polish translations.
|
||||
- [ru.json](ru.json): Russian translations.
|
||||
- [sv.json](sv.json): Swedish translations.
|
||||
|
||||
## Usage
|
||||
|
||||
These files are loaded by the internationalization (i18n) framework used in the project to display the UI in the user's preferred language. English strings are used as defaults directly in the source code. The translation json files are loaded in [../index.tsx](src/index.tsx).
|
||||
These files are loaded by the internationalization (i18n) framework used in the
|
||||
project to display the UI in the user's preferred language. English strings are
|
||||
used as defaults directly in the source code. The translation json files are
|
||||
loaded in [../index.tsx](src/index.tsx).
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
# Utilities (`src/util`)
|
||||
|
||||
This directory contains various utility functions, hooks, and classes used across the application. These utilities handle date calculations, GEDCOM data processing, internationalization of errors, analytics, and responsive design setup.
|
||||
This directory contains various utility functions, hooks, and classes used
|
||||
across the application. These utilities handle date calculations, GEDCOM data
|
||||
processing, internationalization of errors, analytics, and responsive design
|
||||
setup.
|
||||
|
||||
## Files
|
||||
|
||||
- **[age_util.ts](age_util.ts)**: Utilities for calculating and formatting age based on birth and death dates. It handles exact dates, ranges, and qualifiers, and supports localization.
|
||||
- **[age_util.ts](age_util.ts)**: Utilities for calculating and formatting age
|
||||
based on birth and death dates. It handles exact dates, ranges, and
|
||||
qualifiers, and supports localization.
|
||||
- **[age_util.spec.ts](age_util.spec.ts)**: Unit tests for `age_util.ts`.
|
||||
- **[analytics.ts](analytics.ts)**: Provides the `analyticsEvent` function to send events to Google Analytics using `gtag`.
|
||||
- **[analytics_noop.ts](analytics_noop.ts)**: A no-op implementation of `analyticsEvent`, used when analytics are disabled.
|
||||
- **[date_util.ts](date_util.ts)**: Utilities for formatting and comparing dates, handling `DateOrRange` objects from the `topola` library, and supporting localization.
|
||||
- **[error.ts](error.ts)**: Defines the `TopolaError` class, extending `Error` to include an error code and arguments used for internationalized error messages.
|
||||
- **[error_i18n.ts](error_i18n.ts)**: Provides `getI18nMessage` to return translated messages for `TopolaError` instances.
|
||||
- **[gedcom_util.ts](gedcom_util.ts)**: A comprehensive utility file for handling GEDCOM data. It includes functions for parsing and converting GEDCOM files, normalizing data (sorting children and spouses), dereferencing entries, and extracting specific data like names and sources.
|
||||
- **[gedcom_util.spec.ts](gedcom_util.spec.ts)**: Unit tests for `gedcom_util.ts`.
|
||||
- **[media.ts](media.ts)**: Sets up responsive design breakpoints (small and large) using the `@artsy/fresnel` library.
|
||||
- **[previous-hook.ts](previous-hook.ts)**: A custom React hook (`usePrevious`) that returns the value of a variable from the previous render.
|
||||
- **[analytics.ts](analytics.ts)**: Provides the `analyticsEvent` function to
|
||||
send events to Google Analytics using `gtag`.
|
||||
- **[analytics_noop.ts](analytics_noop.ts)**: A no-op implementation of
|
||||
`analyticsEvent`, used when analytics are disabled.
|
||||
- **[date_util.ts](date_util.ts)**: Utilities for formatting and comparing
|
||||
dates, handling `DateOrRange` objects from the `topola` library, and
|
||||
supporting localization.
|
||||
- **[error.ts](error.ts)**: Defines the `TopolaError` class, extending `Error`
|
||||
to include an error code and arguments used for internationalized error
|
||||
messages.
|
||||
- **[error_i18n.ts](error_i18n.ts)**: Provides `getI18nMessage` to return
|
||||
translated messages for `TopolaError` instances.
|
||||
- **[gedcom_util.ts](gedcom_util.ts)**: A comprehensive utility file for
|
||||
handling GEDCOM data. It includes functions for parsing and converting GEDCOM
|
||||
files, normalizing data (sorting children and spouses), dereferencing entries,
|
||||
and extracting specific data like names and sources.
|
||||
- **[gedcom_util.spec.ts](gedcom_util.spec.ts)**: Unit tests for
|
||||
`gedcom_util.ts`.
|
||||
- **[media.ts](media.ts)**: Sets up responsive design breakpoints (small and
|
||||
large) using the `@artsy/fresnel` library.
|
||||
- **[previous-hook.ts](previous-hook.ts)**: A custom React hook (`usePrevious`)
|
||||
that returns the value of a variable from the previous render.
|
||||
|
||||
@@ -1,54 +1,101 @@
|
||||
# End-to-End & Visual Testing Suite (`tests`)
|
||||
|
||||
This directory houses all automated test specifications and validation fixtures for validating the **Topola Genealogy Viewer** application. The tests are powered by the modern Playwright framework to offer robust, type-safe end-to-end (E2E) user flows and pixel-perfect visual regression validations.
|
||||
This directory houses all automated test specifications and validation fixtures
|
||||
for validating the **Topola Genealogy Viewer** application. The tests are
|
||||
powered by the modern Playwright framework to offer robust, type-safe end-to-end
|
||||
(E2E) user flows and pixel-perfect visual regression validations.
|
||||
|
||||
## Test Design Reference Documents
|
||||
|
||||
Before writing new or adjusting existing tests, developers should review their corresponding systems design guidelines under the `docs/` directory to align with established conventions and paradigms:
|
||||
* **[PLAYWRIGHT_DESIGN.md](../docs/PLAYWRIGHT_DESIGN.md)**: Explains overall Playwright orchestration, server hooks, mock GEDCOM network intercept routines, tracking blockages, and in-memory iframe environments.
|
||||
* **[SCREENSHOT_TESTS_DESIGN.md](../docs/SCREENSHOT_TESTS_DESIGN.md)**: Outlines visual testing strategies, viewport locks, dynamic footer/changelog sanitization overrides in virtual DOMs, threshold sensitivities, and local baseline update policies.
|
||||
Before writing new or adjusting existing tests, developers should review their
|
||||
corresponding systems design guidelines under the `docs/` directory to align
|
||||
with established conventions and paradigms:
|
||||
|
||||
- **[PLAYWRIGHT_DESIGN.md](../docs/PLAYWRIGHT_DESIGN.md)**: Explains overall
|
||||
Playwright orchestration, server hooks, mock GEDCOM network intercept
|
||||
routines, tracking blockages, and in-memory iframe environments.
|
||||
- **[SCREENSHOT_TESTS_DESIGN.md](../docs/SCREENSHOT_TESTS_DESIGN.md)**: Outlines
|
||||
visual testing strategies, viewport locks, dynamic footer/changelog
|
||||
sanitization overrides in virtual DOMs, threshold sensitivities, and local
|
||||
baseline update policies.
|
||||
|
||||
---
|
||||
|
||||
## Test Specs Registry
|
||||
|
||||
Tests are strategically partitioned into standard functional E2E specs and isolated visual regression specs (which are tagged and grouped into targeted Playwright projects).
|
||||
Tests are strategically partitioned into standard functional E2E specs and
|
||||
isolated visual regression specs (which are tagged and grouped into targeted
|
||||
Playwright projects).
|
||||
|
||||
### 1. Functional E2E Specs
|
||||
Verify functional correctness, route parameters, drawer side-panels, dynamic autocomplete searching, and external/bidirectional event loops.
|
||||
* **[chart_view.spec.ts](chart_view.spec.ts)**: Asserts core tree rendering from mock network parameters, navigation shifts, transition handling, and card node interaction.
|
||||
* **[embedded.spec.ts](embedded.spec.ts)**: Tests embedded iframe operations using virtual server postMessage listeners to execute proper parent-child synchronizations.
|
||||
* **[intro.spec.ts](intro.spec.ts)**: Checks base landing layouts, instructions panels, menu existence, and responsive header navigation buttons.
|
||||
* **[search.spec.ts](search.spec.ts)**: Targets autocompletion components using robust search locators to verify input debouncing and focus updates.
|
||||
* **[upload.spec.ts](upload.spec.ts)**: Validates uploading raw GEDCOM files from the local filesystem, asserting navigation transitions, state parsing, and virtual tree renderings.
|
||||
* **[webmcp.spec.ts](webmcp.spec.ts)**: Evaluates out-of-process browser calls from Model Context Protocol tool registrations and action assertions.
|
||||
|
||||
Verify functional correctness, route parameters, drawer side-panels, dynamic
|
||||
autocomplete searching, and external/bidirectional event loops.
|
||||
|
||||
- **[chart_view.spec.ts](chart_view.spec.ts)**: Asserts core tree rendering from
|
||||
mock network parameters, navigation shifts, transition handling, and card node
|
||||
interaction.
|
||||
- **[embedded.spec.ts](embedded.spec.ts)**: Tests embedded iframe operations
|
||||
using virtual server postMessage listeners to execute proper parent-child
|
||||
synchronizations.
|
||||
- **[intro.spec.ts](intro.spec.ts)**: Checks base landing layouts, instructions
|
||||
panels, menu existence, and responsive header navigation buttons.
|
||||
- **[search.spec.ts](search.spec.ts)**: Targets autocompletion components using
|
||||
robust search locators to verify input debouncing and focus updates.
|
||||
- **[upload.spec.ts](upload.spec.ts)**: Validates uploading raw GEDCOM files
|
||||
from the local filesystem, asserting navigation transitions, state parsing,
|
||||
and virtual tree renderings.
|
||||
- **[webmcp.spec.ts](webmcp.spec.ts)**: Evaluates out-of-process browser calls
|
||||
from Model Context Protocol tool registrations and action assertions.
|
||||
|
||||
### 2. Visual Regression / Screenshot Specs
|
||||
Capture pixel-perfect layout comparisons against preserved snapshots to detect subtle structural overlaps or rendering discrepancies.
|
||||
* **[charts_visual.spec.ts](charts_visual.spec.ts)**: Loops through multiple layouts (`Hourglass`, `Relatives`, `Donatso`) using a local GEDCOM dataset to ensure aesthetic layout alignments.
|
||||
* **[config_visual.spec.ts](config_visual.spec.ts)**: Asserts full-window UI synchronization states while settings choices (like colored genders, hidden node IDs, or compact profiles) are toggled.
|
||||
* **[details_visual.spec.ts](details_visual.spec.ts)**: Checks that intricate information templates (e.g., complex multiline fact headers, embedded picture borders, and source list grids) are structured beautifully under edge cases.
|
||||
* **[intro_visual.spec.ts](intro_visual.spec.ts)**: Validates static landing layouts. Pre-evaluates dynamic sections (e.g. version/commit logs) to replace them with fixed test markers for baseline durability.
|
||||
|
||||
Capture pixel-perfect layout comparisons against preserved snapshots to detect
|
||||
subtle structural overlaps or rendering discrepancies.
|
||||
|
||||
- **[charts_visual.spec.ts](charts_visual.spec.ts)**: Loops through multiple
|
||||
layouts (`Hourglass`, `Relatives`, `Donatso`) using a local GEDCOM dataset to
|
||||
ensure aesthetic layout alignments.
|
||||
- **[config_visual.spec.ts](config_visual.spec.ts)**: Asserts full-window UI
|
||||
synchronization states while settings choices (like colored genders, hidden
|
||||
node IDs, or compact profiles) are toggled.
|
||||
- **[details_visual.spec.ts](details_visual.spec.ts)**: Checks that intricate
|
||||
information templates (e.g., complex multiline fact headers, embedded picture
|
||||
borders, and source list grids) are structured beautifully under edge cases.
|
||||
- **[intro_visual.spec.ts](intro_visual.spec.ts)**: Validates static landing
|
||||
layouts. Pre-evaluates dynamic sections (e.g. version/commit logs) to replace
|
||||
them with fixed test markers for baseline durability.
|
||||
|
||||
---
|
||||
|
||||
## Supporting Infrastructure
|
||||
|
||||
To enable lightweight, reproducible, and offline executions, tests rely on the following configuration, definitions, and test data structures:
|
||||
To enable lightweight, reproducible, and offline executions, tests rely on the
|
||||
following configuration, definitions, and test data structures:
|
||||
|
||||
* **[fixtures/embedded_frame.html](fixtures/embedded_frame.html)**: HTML template for serving cross-origin iframe wrapper mockups virtually to the browser container.
|
||||
* **[global.d.ts](global.d.ts)**: TypeScript declarations defining window overrides (like AI registration pointers `window.__registeredTools`) to bypass compiler warnings.
|
||||
* **[helpers.ts](helpers.ts)**: Unified routing utilities:
|
||||
* `setupHermeticEnvironment()`: Intercepts and halts metrics/analytical queries and embeds local baseline fonts during spec executions.
|
||||
* `setupGedcomRoute()`: Re-routes standard genealogy payload network paths directly to load standard local datasets on-the-fly (`src/datasource/testdata/test.ged`).
|
||||
* **[tsconfig.json](tsconfig.json)**: Typecheck preferences custom to the Playwright runner environment to avoid compilation type collisions.
|
||||
* **`[spec_name]-snapshots/`**: Directory structure containing expected baseline references and image comparison files.
|
||||
- **[fixtures/embedded_frame.html](fixtures/embedded_frame.html)**: HTML
|
||||
template for serving cross-origin iframe wrapper mockups virtually to the
|
||||
browser container.
|
||||
- **[global.d.ts](global.d.ts)**: TypeScript declarations defining window
|
||||
overrides (like AI registration pointers `window.__registeredTools`) to bypass
|
||||
compiler warnings.
|
||||
- **[helpers.ts](helpers.ts)**: Unified routing utilities:
|
||||
- `setupHermeticEnvironment()`: Intercepts and halts metrics/analytical
|
||||
queries and embeds local baseline fonts during spec executions.
|
||||
- `setupGedcomRoute()`: Re-routes standard genealogy payload network paths
|
||||
directly to load standard local datasets on-the-fly
|
||||
(`src/datasource/testdata/test.ged`).
|
||||
- **[tsconfig.json](tsconfig.json)**: Typecheck preferences custom to the
|
||||
Playwright runner environment to avoid compilation type collisions.
|
||||
- **`[spec_name]-snapshots/`**: Directory structure containing expected baseline
|
||||
references and image comparison files.
|
||||
|
||||
---
|
||||
|
||||
## Running the Tests
|
||||
|
||||
Automated scripts to run and update tests are integrated into standard workspace npm configurations:
|
||||
Automated scripts to run and update tests are integrated into standard workspace
|
||||
npm configurations:
|
||||
|
||||
```bash
|
||||
# Run standard functional E2E tests (fast verification)
|
||||
|
||||
@@ -10,21 +10,22 @@ WikiTree API, CORS proxy, and GitHub raw URLs.
|
||||
npm run test:probers
|
||||
```
|
||||
|
||||
This uses the separate `playwright.prober.config.ts` configuration, which
|
||||
does not start a local dev server. Each spec navigates to a full absolute
|
||||
URL (or `localhost:8080` for the Docker prober).
|
||||
This uses the separate `playwright.prober.config.ts` configuration, which does
|
||||
not start a local dev server. Each spec navigates to a full absolute URL (or
|
||||
`localhost:8080` for the Docker prober).
|
||||
|
||||
## What They Test
|
||||
|
||||
| Spec | Target | Exercises |
|
||||
| --- | --- | --- |
|
||||
| `gh-pages-gedcom.spec.ts` | `pewu.github.io` | GitHub Pages deployment, CORS proxy, GEDCOM-from-URL |
|
||||
| `wikitree.spec.ts` | `apps.wikitree.com` | WikiTree direct API, WikiTree deployment |
|
||||
| `wikitree-cors-gedcom.spec.ts` | `apps.wikitree.com` | WikiTree deployment, CORS proxy via GEDCOM-from-URL |
|
||||
| `docker.spec.ts` | `localhost:8080` | Published Docker image from GHCR |
|
||||
| Spec | Target | Exercises |
|
||||
| ------------------------------ | ------------------- | ---------------------------------------------------- |
|
||||
| `gh-pages-gedcom.spec.ts` | `pewu.github.io` | GitHub Pages deployment, CORS proxy, GEDCOM-from-URL |
|
||||
| `wikitree.spec.ts` | `apps.wikitree.com` | WikiTree direct API, WikiTree deployment |
|
||||
| `wikitree-cors-gedcom.spec.ts` | `apps.wikitree.com` | WikiTree deployment, CORS proxy via GEDCOM-from-URL |
|
||||
| `docker.spec.ts` | `localhost:8080` | Published Docker image from GHCR |
|
||||
|
||||
## Notes
|
||||
|
||||
- Probers run sequentially (`fullyParallel: false`) to avoid rate-limiting.
|
||||
- Each prober has its own GitHub Actions workflow for independent triggering.
|
||||
- Probers do not block Google Analytics, so live runs generate real analytics events.
|
||||
- Probers do not block Google Analytics, so live runs generate real analytics
|
||||
events.
|
||||
|
||||
Reference in New Issue
Block a user