Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions er/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ build/Release

# Dependency directories
node_modules/

# Playwright (regression net)
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
Expand Down
22 changes: 22 additions & 0 deletions er/e2e/fixtures/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"server": "mock.local",
"public_name": "test",
"map_title": "Test Tracker",
"color_scheme": ["#DB2222", "#219DB8", "#E69E39"],
"map": {
"center": [0, 0],
"zoom": 2,
"pitch": 0,
"show_subject_names": true,
"map_icon_size": 30
},
"subjects": {
"sub-1": {
"name": "Zola",
"age": "5 years",
"fun_fact": "Zola loves acacia leaves.",
"pictures": [{ "path": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" }],
"detail_description": "<p>Zola is a <b>test</b> giraffe.</p>"
}
}
}
36 changes: 36 additions & 0 deletions er/e2e/fixtures/subjects.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"data": {
"data": [
{
"id": "sub-1",
"name": "Zola",
"common_name": null,
"subject_subtype": "giraffe",
"sex": "female",
"last_position": {
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [0.01, 0.01] },
"properties": {
"DateTime": "2026-06-01T08:30:00",
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
}
}
},
{
"id": "sub-2",
"name": "Mara",
"common_name": null,
"subject_subtype": "giraffe",
"sex": "female",
"last_position": {
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-0.02, 0.015] },
"properties": {
"DateTime": "2026-06-01T09:15:00",
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
}
}
}
]
}
}
14 changes: 14 additions & 0 deletions er/e2e/fixtures/tracks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"data": {
"features": [
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[0.01, 0.01], [0.005, 0.005], [0.0, 0.0]]
},
"properties": { "id": "sub-1" }
}
]
}
}
94 changes: 94 additions & 0 deletions er/e2e/journeys.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { test, expect, Page } from '@playwright/test';
import config from './fixtures/config.json' assert { type: 'json' };
import subjects from './fixtures/subjects.json' assert { type: 'json' };
import tracks from './fixtures/tracks.json' assert { type: 'json' };

// Regression net for the CURRENT app (PR 1.1). These journeys assert behavior through
// the UI, so they survive the Vite/React 19/MapLibre overhaul. All network is mocked so
// the suite is deterministic and independent of the live PADAS server.
test.beforeEach(async ({ page }) => {
await page.route('**/config/config.json', (route) => route.fulfill({ json: config }));
await page.route('**/api/v1.0/subjects', (route) => route.fulfill({ json: subjects }));
await page.route('**/tracks', (route) => route.fulfill({ json: tracks }));
});

// Open the legend and wait for subjects (fetched inside the Mapbox "load" event) to render.
async function openLegendWithSubjects(page: Page) {
await page.goto('/');
await page.locator('#legend-open-button').click();
await expect(page.getByText('Zola')).toBeVisible({ timeout: 20_000 });
}

// Wrap window.GlobalMap.flyTo so we can assert it was called (and with what) without
// depending on pixel-level WebGL rendering.
async function spyFlyTo(page: Page) {
await page.evaluate(() => {
const m = (window as any).GlobalMap;
(window as any).__flyTo = null;
const orig = m.flyTo.bind(m);
m.flyTo = (opts: any) => { (window as any).__flyTo = opts; return orig(opts); };
});
}

test('1. app loads: static controls render', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#legend-open-button')).toBeVisible();
await expect(page.locator('#tips-button-container')).toBeVisible();
});

test('2. legend populates from subjects', async ({ page }) => {
await openLegendWithSubjects(page);
await expect(page.getByText('Mara')).toBeVisible();
});

test('3. clicking a subject name opens its story panel', async ({ page }) => {
await openLegendWithSubjects(page);
await page.locator('#Zola').first().click();
await expect(page.getByText('Back')).toBeVisible();
await expect(page.getByText(/test giraffe/i)).toBeVisible();
});

test('4. toggling the track button requests that subject\'s tracks', async ({ page }) => {
await openLegendWithSubjects(page);
const trackRequest = page.waitForRequest('**/tracks');
await page.locator('#subject-track-button').first().click();
await trackRequest; // resolves only if the track-display flow fired
});

test('5. location button flies the map to the subject\'s coordinates', async ({ page }) => {
await openLegendWithSubjects(page);
await spyFlyTo(page);
await page.locator('#subject-location-button').first().click();
const opts = await page.evaluate(() => (window as any).__flyTo);
expect(opts).not.toBeNull();
expect(opts.center).toEqual([0.01, 0.01]);
});

test('6. help tips open and close', async ({ page }) => {
await page.goto('/');
await expect(page.getByText('Helpful Tips')).toBeHidden();
await page.locator('#tips-button-container').click();
await expect(page.getByText('Helpful Tips')).toBeVisible();
await page.locator('#close-icon').click();
await expect(page.getByText('Helpful Tips')).toBeHidden();
});

test('7. Alt+R hotkey resets the map orientation', async ({ page }) => {
await openLegendWithSubjects(page);
await spyFlyTo(page);
// The hotkey handler lives on #map-container (onKeyDown). Dispatch native keydowns
// with the keyCodes the app checks for: Alt (18) then R (82).
await page.evaluate(() => {
const el = document.getElementById('map-container')!;
const fire = (keyCode: number) => {
const ev = new KeyboardEvent('keydown', { bubbles: true });
Object.defineProperty(ev, 'keyCode', { get: () => keyCode });
el.dispatchEvent(ev);
};
fire(18);
fire(82);
});
const opts = await page.evaluate(() => (window as any).__flyTo);
expect(opts).not.toBeNull();
expect(opts.bearing).toBe(0);
});
4 changes: 3 additions & 1 deletion er/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"scripts": {
"start": "webpack-dev-server --mode development --open --host localhost",
"build": "webpack --mode production",
"lint": "eslint --cache --format codeframe --ext mjs,jsx,js src"
"lint": "eslint --cache --format codeframe --ext mjs,jsx,js src",
"test:e2e": "playwright test"
},
"dependencies": {
"mapbox-gl": "2.1.1",
Expand All @@ -21,6 +22,7 @@
"@neutrinojs/dev-server": "^9.5.0",
"@neutrinojs/react": "^9.5.0",
"@neutrinojs/standardjs": "^9.5.0",
"@playwright/test": "^1.60.0",
"eslint": "^7",
"neutrino": "^9.5.0",
"webpack": "^4",
Expand Down
27 changes: 27 additions & 0 deletions er/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
reporter: 'list',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
],
webServer: {
// ⚠️ REMOVE-AFTER-VITE (overhaul Pass 1.2): webpack 4 (Neutrino 9) crashes on
// Node 17+ with ERR_OSSL_EVP_UNSUPPORTED. --openssl-legacy-provider props up the
// legacy dev server just long enough to run this regression net. Once the app
// builds on Vite, delete the NODE_OPTIONS prefix below (ideally this whole flag).
// Tracking: workspace/design-docs/2026-06-03-app-overhaul-design.md (Pass 1.2).
command: 'NODE_OPTIONS=--openssl-legacy-provider npx webpack-dev-server --mode development --host localhost --port 3000',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
31 changes: 26 additions & 5 deletions er/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,13 @@
babel-merge "^3.0.0"
deepmerge "^1.5.2"

"@playwright/test@^1.60.0":
version "1.60.0"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.60.0.tgz#e696c31427e8882851235cd556dc2490c3206d97"
integrity sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==
dependencies:
playwright "1.60.0"

"@types/glob@^7.1.1":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
Expand Down Expand Up @@ -3445,6 +3452,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=

fsevents@2.3.2, fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==

fsevents@^1.2.7:
version "1.2.13"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38"
Expand All @@ -3453,11 +3465,6 @@ fsevents@^1.2.7:
bindings "^1.5.0"
nan "^2.12.1"

fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==

function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
Expand Down Expand Up @@ -5348,6 +5355,20 @@ pkg-dir@^4.1.0:
dependencies:
find-up "^4.0.0"

playwright-core@1.60.0:
version "1.60.0"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.60.0.tgz#24e0d9cc4730713db5dffcace29b5e4696b1907a"
integrity sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==

playwright@1.60.0:
version "1.60.0"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.60.0.tgz#89710863a51f21112633ef8b6b182594d3bfd7b5"
integrity sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==
dependencies:
playwright-core "1.60.0"
optionalDependencies:
fsevents "2.3.2"

portfinder@^1.0.26:
version "1.0.28"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
Expand Down