diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
new file mode 100644
index 00000000..0b87512d
--- /dev/null
+++ b/.github/workflows/playwright.yml
@@ -0,0 +1,25 @@
+name: Playwright Tests
+on:
+ pull_request:
+ branches: [ master ]
+jobs:
+ test:
+ timeout-minutes: 60
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-node@v5
+ with:
+ node-version: lts/*
+ - name: Install dependencies
+ run: npm ci
+ - name: Install Playwright Browsers
+ run: playwright install --with-deps
+ - name: Run Playwright tests
+ run: playwright test
+ - uses: actions/upload-artifact@v4
+ if: ${{ !cancelled() }}
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 30
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index fded70c1..c730ec13 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@
*/node_modules
/dist
/coverage
+/playwright-report
+/test-results
\ No newline at end of file
diff --git a/e2e/layers.spec.ts b/e2e/layers.spec.ts
new file mode 100644
index 00000000..a9fa0901
--- /dev/null
+++ b/e2e/layers.spec.ts
@@ -0,0 +1,267 @@
+import { test, expect } from '@playwright/test'
+
+test.describe('map layers', () => {
+ test.beforeEach(async ({ context, page }) => {
+ // Clear all storage to ensure clean state
+ await context.clearCookies()
+
+ await page.goto('/')
+ await page.evaluate(() => {
+ localStorage.clear()
+ sessionStorage.clear()
+ })
+
+ // Navigate with seed parameter
+ await page.goto('/?seed=test-seed')
+
+ const mapElement = page.locator('#map')
+ await expect(mapElement).toBeVisible()
+
+ // Wait for map generation to complete
+ await expect(mapElement.locator('#terrs')).toBeAttached({ timeout: 30000 })
+ await expect(mapElement.locator('#labels')).toBeAttached()
+ await expect(page.locator('#loading')).toBeHidden({ timeout: 30000 })
+ await page.waitForTimeout(1000)
+ })
+
+ // Ocean and water layers
+ test('ocean layer', async ({ page }) => {
+ const ocean = page.locator('#ocean')
+ await expect(ocean).toBeAttached()
+ const html = await ocean.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('ocean.html')
+ })
+
+ test('lakes layer', async ({ page }) => {
+ const lakes = page.locator('#lakes')
+ await expect(lakes).toBeAttached()
+ const html = await lakes.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('lakes.html')
+ })
+
+ test('coastline layer', async ({ page }) => {
+ const coastline = page.locator('#coastline')
+ await expect(coastline).toBeAttached()
+ const html = await coastline.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('coastline.html')
+ })
+
+ // Terrain and heightmap layers
+ test('terrain layer', async ({ page }) => {
+ const terrs = page.locator('#terrs')
+ await expect(terrs).toBeAttached()
+ const html = await terrs.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('terrain.html')
+ })
+
+ test('landmass layer', async ({ page }) => {
+ const landmass = page.locator('#landmass')
+ await expect(landmass).toBeAttached()
+ const html = await landmass.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('landmass.html')
+ })
+
+ // Climate and environment layers
+ test('biomes layer', async ({ page }) => {
+ const biomes = page.locator('#biomes')
+ await expect(biomes).toBeAttached()
+ const html = await biomes.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('biomes.html')
+ })
+
+ test('ice layer', async ({ page }) => {
+ const ice = page.locator('#ice')
+ await expect(ice).toBeAttached()
+ const html = await ice.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('ice.html')
+ })
+
+ test('temperature layer', async ({ page }) => {
+ const temperature = page.locator('#temperature')
+ await expect(temperature).toBeAttached()
+ const html = await temperature.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('temperature.html')
+ })
+
+ test('precipitation layer', async ({ page }) => {
+ const prec = page.locator('#prec')
+ await expect(prec).toBeAttached()
+ const html = await prec.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('precipitation.html')
+ })
+
+ // Geographic features
+ test('rivers layer', async ({ page }) => {
+ const rivers = page.locator('#rivers')
+ await expect(rivers).toBeAttached()
+ const html = await rivers.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('rivers.html')
+ })
+
+ test('relief layer', async ({ page }) => {
+ const terrain = page.locator('#terrain')
+ await expect(terrain).toBeAttached()
+ const html = await terrain.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('relief.html')
+ })
+
+ // Political layers
+ test('states/regions layer', async ({ page }) => {
+ const regions = page.locator('#regions')
+ await expect(regions).toBeAttached()
+ const html = await regions.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('regions.html')
+ })
+
+ test('provinces layer', async ({ page }) => {
+ const provs = page.locator('#provs')
+ await expect(provs).toBeAttached()
+ const html = await provs.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('provinces.html')
+ })
+
+ test('borders layer', async ({ page }) => {
+ const borders = page.locator('#borders')
+ await expect(borders).toBeAttached()
+ const html = await borders.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('borders.html')
+ })
+
+ // Cultural layers
+ test('cultures layer', async ({ page }) => {
+ const cults = page.locator('#cults')
+ await expect(cults).toBeAttached()
+ const html = await cults.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('cultures.html')
+ })
+
+ test('religions layer', async ({ page }) => {
+ const relig = page.locator('#relig')
+ await expect(relig).toBeAttached()
+ const html = await relig.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('religions.html')
+ })
+
+ // Infrastructure layers
+ test('routes layer', async ({ page }) => {
+ const routes = page.locator('#routes')
+ await expect(routes).toBeAttached()
+ const html = await routes.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('routes.html')
+ })
+
+ // Settlement layers
+ test('burgs/icons layer', async ({ page }) => {
+ const icons = page.locator('#icons')
+ await expect(icons).toBeAttached()
+ const html = await icons.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('icons.html')
+ })
+
+ test('anchors layer', async ({ page }) => {
+ const anchors = page.locator('#anchors')
+ await expect(anchors).toBeAttached()
+ const html = await anchors.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('anchors.html')
+ })
+
+ // Labels layer (without text content due to font rendering)
+ test('labels layer', async ({ page }) => {
+ const labels = page.locator('#labels')
+ await expect(labels).toBeAttached()
+ // Remove text content but keep structure (text rendering varies)
+ const html = await labels.evaluate((el) => {
+ const clone = el.cloneNode(true) as Element
+ clone.querySelectorAll('text, tspan').forEach((t) => t.remove())
+ return clone.outerHTML
+ })
+ expect(html).toMatchSnapshot('labels.html')
+ })
+
+ // Military and markers
+ test('markers layer', async ({ page }) => {
+ const markers = page.locator('#markers')
+ await expect(markers).toBeAttached()
+ const html = await markers.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('markers.html')
+ })
+
+ test('armies layer', async ({ page }) => {
+ const armies = page.locator('#armies')
+ await expect(armies).toBeAttached()
+ const html = await armies.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('armies.html')
+ })
+
+ // Special features
+ test('zones layer', async ({ page }) => {
+ const zones = page.locator('#zones')
+ await expect(zones).toBeAttached()
+ const html = await zones.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('zones.html')
+ })
+
+ test('emblems layer', async ({ page }) => {
+ const emblems = page.locator('#emblems')
+ await expect(emblems).toBeAttached()
+ const html = await emblems.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('emblems.html')
+ })
+
+ // Grid and coordinates
+ test('cells layer', async ({ page }) => {
+ const cells = page.locator('g#cells')
+ await expect(cells).toBeAttached()
+ const html = await cells.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('cells.html')
+ })
+
+ test('coordinates layer', async ({ page }) => {
+ const coordinates = page.locator('#coordinates')
+ await expect(coordinates).toBeAttached()
+ const html = await coordinates.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('coordinates.html')
+ })
+
+ test('compass layer', async ({ page }) => {
+ const compass = page.locator('#compass')
+ await expect(compass).toBeAttached()
+ const html = await compass.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('compass.html')
+ })
+
+ // UI elements
+ test('scale bar layer', async ({ page }) => {
+ const scaleBar = page.locator('#scaleBar')
+ await expect(scaleBar).toBeAttached()
+ // Scale bar has randomized distances, snapshot structure only
+ const html = await scaleBar.evaluate((el) => {
+ const clone = el.cloneNode(true) as Element
+ clone.querySelectorAll('text').forEach((t) => t.remove())
+ return clone.outerHTML
+ })
+ expect(html).toMatchSnapshot('scaleBar.html')
+ })
+
+ test('ruler layer', async ({ page }) => {
+ const ruler = page.locator('#ruler')
+ await expect(ruler).toBeAttached()
+ const html = await ruler.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('ruler.html')
+ })
+
+ test('vignette layer', async ({ page }) => {
+ const vignette = page.locator('#vignette')
+ await expect(vignette).toBeAttached()
+ const html = await vignette.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('vignette.html')
+ })
+
+ // Population layer
+ test('population layer', async ({ page }) => {
+ const population = page.locator('#population')
+ await expect(population).toBeAttached()
+ const html = await population.evaluate((el) => el.outerHTML)
+ expect(html).toMatchSnapshot('population.html')
+ })
+})
diff --git a/e2e/layers.spec.ts-snapshots/anchors-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/anchors-chromium-darwin.html
new file mode 100644
index 00000000..7c2df822
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/anchors-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/armies-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/armies-chromium-darwin.html
new file mode 100644
index 00000000..face6396
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/armies-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/biomes-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/biomes-chromium-darwin.html
new file mode 100644
index 00000000..582a9c1d
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/biomes-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/borders-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/borders-chromium-darwin.html
new file mode 100644
index 00000000..92b98dc8
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/borders-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/cells-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/cells-chromium-darwin.html
new file mode 100644
index 00000000..d73d9b2f
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/cells-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/coastline-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/coastline-chromium-darwin.html
new file mode 100644
index 00000000..7a2c4c51
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/coastline-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/compass-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/compass-chromium-darwin.html
new file mode 100644
index 00000000..3c0892a6
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/compass-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/coordinates-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/coordinates-chromium-darwin.html
new file mode 100644
index 00000000..48e6c40b
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/coordinates-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/cultures-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/cultures-chromium-darwin.html
new file mode 100644
index 00000000..193726a3
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/cultures-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/emblems-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/emblems-chromium-darwin.html
new file mode 100644
index 00000000..1de7ef9d
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/emblems-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/ice-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/ice-chromium-darwin.html
new file mode 100644
index 00000000..72ac77e8
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/ice-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/icons-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/icons-chromium-darwin.html
new file mode 100644
index 00000000..629db2b0
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/icons-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/labels-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/labels-chromium-darwin.html
new file mode 100644
index 00000000..6ffcf3b9
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/labels-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/lakes-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/lakes-chromium-darwin.html
new file mode 100644
index 00000000..cce3f70e
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/lakes-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/landmass-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/landmass-chromium-darwin.html
new file mode 100644
index 00000000..ec70a34e
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/landmass-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/markers-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/markers-chromium-darwin.html
new file mode 100644
index 00000000..100a1e3f
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/markers-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/ocean-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/ocean-chromium-darwin.html
new file mode 100644
index 00000000..b950e1a7
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/ocean-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/population-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/population-chromium-darwin.html
new file mode 100644
index 00000000..10175492
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/population-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/precipitation-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/precipitation-chromium-darwin.html
new file mode 100644
index 00000000..8ab517cb
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/precipitation-chromium-darwin.html
@@ -0,0 +1 @@
+⇇⇉⇇⇇⇉⇇⇊⇈
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/provinces-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/provinces-chromium-darwin.html
new file mode 100644
index 00000000..3fe87d6e
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/provinces-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/regions-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/regions-chromium-darwin.html
new file mode 100644
index 00000000..848ce4a3
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/regions-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/relief-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/relief-chromium-darwin.html
new file mode 100644
index 00000000..6883fe5b
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/relief-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/religions-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/religions-chromium-darwin.html
new file mode 100644
index 00000000..85c96e30
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/religions-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/rivers-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/rivers-chromium-darwin.html
new file mode 100644
index 00000000..087b4d8d
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/rivers-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/routes-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/routes-chromium-darwin.html
new file mode 100644
index 00000000..6739112c
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/routes-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/ruler-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/ruler-chromium-darwin.html
new file mode 100644
index 00000000..755b2d65
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/ruler-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/scaleBar-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/scaleBar-chromium-darwin.html
new file mode 100644
index 00000000..9ebec74e
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/scaleBar-chromium-darwin.html
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/temperature-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/temperature-chromium-darwin.html
new file mode 100644
index 00000000..36464dbd
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/temperature-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/terrain-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/terrain-chromium-darwin.html
new file mode 100644
index 00000000..bc13f8be
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/terrain-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/vignette-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/vignette-chromium-darwin.html
new file mode 100644
index 00000000..6eaf80a4
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/vignette-chromium-darwin.html
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/e2e/layers.spec.ts-snapshots/zones-chromium-darwin.html b/e2e/layers.spec.ts-snapshots/zones-chromium-darwin.html
new file mode 100644
index 00000000..14cd5141
--- /dev/null
+++ b/e2e/layers.spec.ts-snapshots/zones-chromium-darwin.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 9af3fd78..67512031 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,8 +15,10 @@
"polylabel": "^2.0.1"
},
"devDependencies": {
+ "@playwright/test": "^1.57.0",
"@types/d3": "^7.4.3",
"@types/delaunator": "^5.0.3",
+ "@types/node": "^25.0.10",
"@types/polylabel": "^1.1.3",
"@vitest/browser": "^4.0.18",
"@vitest/browser-playwright": "^4.0.18",
@@ -478,6 +480,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@playwright/test": {
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
+ "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.57.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@polka/url": {
"version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
@@ -1165,6 +1183,17 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "25.0.10",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz",
+ "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
"node_modules/@types/polylabel": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.1.3.tgz",
@@ -1997,6 +2026,7 @@
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"playwright-core": "1.57.0"
},
@@ -2269,6 +2299,13 @@
"node": ">=14.17"
}
},
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/vite": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
diff --git a/package.json b/package.json
index 1c802071..9d3fbe11 100644
--- a/package.json
+++ b/package.json
@@ -18,11 +18,14 @@
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest",
- "test:browser": "vitest --config=vitest.browser.config.ts"
+ "test:browser": "vitest --config=vitest.browser.config.ts",
+ "test:e2e": "playwright test"
},
"devDependencies": {
+ "@playwright/test": "^1.57.0",
"@types/d3": "^7.4.3",
"@types/delaunator": "^5.0.3",
+ "@types/node": "^25.0.10",
"@types/polylabel": "^1.1.3",
"@vitest/browser": "^4.0.18",
"@vitest/browser-playwright": "^4.0.18",
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 00000000..558f6b3f
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,28 @@
+import { defineConfig, devices } from '@playwright/test'
+
+export default defineConfig({
+ testDir: './e2e',
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 1 : undefined,
+ reporter: [['html', { open: 'always' }]],
+ use: {
+ baseURL: 'http://localhost:4173',
+ trace: 'on-first-retry',
+ // Fixed viewport to ensure consistent map rendering
+ viewport: { width: 1280, height: 720 },
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+ webServer: {
+ command: 'npm run build && npm run preview',
+ url: 'http://localhost:4173',
+ reuseExistingServer: !process.env.CI,
+ timeout: 120000,
+ },
+})
diff --git a/tsconfig.json b/tsconfig.json
index 8b583a9d..01672af5 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -22,5 +22,6 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
- "include": ["src"]
+ "include": ["src"],
+ "exclude": ["src/e2e"]
}
\ No newline at end of file