From abfcebb5e8bda97db6f0a199125d28fd78a83a55 Mon Sep 17 00:00:00 2001 From: n30nex Date: Sun, 13 Sep 2026 10:09:11 -0400 Subject: [PATCH] Refine map layers and cross-view UI usability (#16) * Refine layer controls and cross-view UI accessibility * Complete cumulative labels and landscape layer usability checks * Keep inactive layer labels readable on light scenes * Check cumulative observation wording in route inspection --- .env.example | 2 +- VERSION | 2 +- compose.build.yml | 2 +- compose.yml | 2 +- docs/map-customization.md | 12 ++- docs/roadmap.md | 8 +- docs/ui-ux-audit-2026-09-13.md | 67 ++++++++++++ web/e2e/customization.spec.ts | 1 + web/e2e/mapControls.ts | 5 + web/e2e/refinements.spec.ts | 1 + web/e2e/tablet-portrait.spec.ts | 1 + web/e2e/ui-ux.spec.ts | 174 ++++++++++++++++++++++++++++++++ web/e2e/visual-smoke.spec.ts | 2 +- web/index.html | 95 ++++++++++------- web/netgraph/index.html | 11 +- web/package-lock.json | 4 +- web/package.json | 2 +- web/src/audio.ts | 3 +- web/src/browserStorage.test.ts | 16 +++ web/src/browserStorage.ts | 14 +++ web/src/display.css | 2 + web/src/displayControls.ts | 9 +- web/src/main.ts | 64 +++++++++--- web/src/map.ts | 5 +- web/src/netgraph/main.ts | 24 ++++- web/src/netgraph/renderer.ts | 7 ++ web/src/netgraph/styles.css | 18 ++++ web/src/nodeInspector.test.ts | 8 +- web/src/nodeInspector.ts | 11 +- web/src/styles.css | 17 +++- 30 files changed, 508 insertions(+), 81 deletions(-) create mode 100644 docs/ui-ux-audit-2026-09-13.md create mode 100644 web/e2e/ui-ux.spec.ts create mode 100644 web/src/browserStorage.test.ts create mode 100644 web/src/browserStorage.ts diff --git a/.env.example b/.env.example index 1d8a9f8..d1e5026 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ # Copy this file to .env and configure your MQTT feed. No map key or build is needed. # Keep .env and any optional .secrets files private. -CARTOLITE_IMAGE=ghcr.io/n30nex/cartolite-server:0.5.0 +CARTOLITE_IMAGE=ghcr.io/n30nex/cartolite-server:0.5.1 CARTOLITE_BIND_ADDR=127.0.0.1 CARTOLITE_PORT=8080 # Optional CARTO override: see compose.carto.yml and docs/deployment.md. diff --git a/VERSION b/VERSION index 8f0916f..4b9fcbe 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.0 +0.5.1 diff --git a/compose.build.yml b/compose.build.yml index 71035dc..0a4140c 100644 --- a/compose.build.yml +++ b/compose.build.yml @@ -6,6 +6,6 @@ services: build: context: . args: - APP_VERSION: ${CARTOLITE_VERSION:-0.5.0} + APP_VERSION: ${CARTOLITE_VERSION:-0.5.1} GIT_SHA: ${CARTOLITE_GIT_SHA:-local} BUILD_TIME: ${CARTOLITE_BUILD_TIME:-unknown} diff --git a/compose.yml b/compose.yml index 5d8831f..b4a9c9d 100644 --- a/compose.yml +++ b/compose.yml @@ -2,7 +2,7 @@ name: cartolite-server services: cartolite: - image: ${CARTOLITE_IMAGE:-ghcr.io/n30nex/cartolite-server:0.5.0} + image: ${CARTOLITE_IMAGE:-ghcr.io/n30nex/cartolite-server:0.5.1} restart: unless-stopped ports: - "${CARTOLITE_BIND_ADDR:-127.0.0.1}:${CARTOLITE_PORT:-8080}:8080" diff --git a/docs/map-customization.md b/docs/map-customization.md index b9cc826..92f2dc0 100644 --- a/docs/map-customization.md +++ b/docs/map-customization.md @@ -1,6 +1,6 @@ # Map, Netgraph, and display settings -Map keeps its everyday controls in the toolbar. Open **Map** for layers, appearance and camera settings. Netgraph has a matching **Display** menu. Menus scroll within the viewport; Escape closes them and returns keyboard focus to their button. +Map keeps its everyday controls in the toolbar. Open **Layers** for grouped Traffic and Map details controls. Expand **Terrain & buildings** or **Appearance** for their additional settings. Netgraph has a matching **Display** menu. Menus scroll within the viewport; Escape closes them and returns keyboard focus to their button. ## Shared appearance @@ -31,3 +31,13 @@ A map notice explains unavailable tile details and offers Retry; traffic data an Synthetic Actions checks cover themes, presets, storage migration and cross-tab updates, keyboard/menu behavior, actual building pixels, terrain/date-line projection, route/packet visibility, bounded performance and the public privacy contract. OpenFreeMap supplies the default geography and building layer. Optional CARTO configuration is described in [Deployment](deployment.md). Netgraph areas remain coordinate-derived global grid squares. + +## Layer guidance and reset + +Toggles show On or Off, and the menu reports how many layers are enabled. Buildings explain when neighborhood zoom is required. Camera and terrain-height controls require 3D; the shading slider requires Topo. + +**Reset layers** restores layer visibility, route window, and shading defaults while keeping the scene, route preset, and sound settings. **Reset all display settings**, under Appearance, also resets the shared Map/Netgraph appearance. Sound remains separate. + +Netgraph supports arrow-key pan, plus/minus zoom, and Home to fit when its canvas has keyboard focus. Its menus remain mutually exclusive for keyboard and pointer use, and phone summaries keep visible labels. + +If browser storage is blocked, the views continue running; preferences are then temporary for the current page. diff --git a/docs/roadmap.md b/docs/roadmap.md index d4f11de..4aaf180 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,5 +1,7 @@ # Audit roadmap +Latest UI review: [2026-09-13 audit and refinement](ui-ux-audit-2026-09-13.md). The paired refinement implements A07, A08, A10 and part of A09, with layer organization and scoped reset; release evidence is separate. + Updated: 12 September 2026. This roadmap turns the end-to-end audit into tracked delivery work. Audit IDs A01–A12 are retained; F IDs track features and R IDs track validation risks. This is the worldwide source-release roadmap. Shared work is paired with [CartoLite Canada](https://github.com/n30nex/CartoLite); A04/O1 is tracked upstream and is not an operator rollout for this repository. @@ -36,10 +38,10 @@ Each item needs its acceptance check, edition parity, and an exact CI result bef | A04 | P1 | Canada | Needs setup | Configure the documented encrypted off-host backup and restore path. Prove a checksummed isolated restore with the matching image/version and independent monitoring; preserve production throughout validation. | | A05 | P2 | Both | Implemented | Reject non-finite RF/coordinate values and impose a bounded MQTT envelope before normalization. Synthetic invalid numbers and over-limit JSON/hex are rejected; valid RSSI-only and SNR-only packets still work. | | A06 | P2 | Both | Implemented | Run retention maintenance during quiet periods and avoid unnecessary checkpoint writes. With no new messages, expired routes/nodes leave the API, checkpoint and connected client; quiet connected feeds remain ready. | -| A07 | P2 | Both | Planned | Label cumulative observation totals honestly and describe windows as “links heard within this period.” Never imply current totals are unique packets counted within the chosen interval. | -| A08 | P2 | Both map pages | Planned | Guard access to browser storage itself and fall back to in-memory preferences. A throwing storage getter cannot prevent startup or control use. | +| A07 | P2 | Both | Implemented | Label cumulative observation totals honestly and describe windows as “links heard within this period.” Never imply current totals are unique packets counted within the chosen interval. | +| A08 | P2 | Both map pages | Implemented | Guard access to browser storage itself and fall back to in-memory preferences. A throwing storage getter cannot prevent startup or control use. | | A09 | P2 | Both | Planned | Transfer keyboard selection focus to details, restore it on close, add Netgraph keyboard pan/zoom/reset, and bounded node/area browsing. Find → details → neighbour → close works without a pointer. | -| A10 | P2 | Both Netgraphs | Planned | Give every phone counter a visible or accessible name; prefer a concise nodes/links summary with optional area/component detail. Validate at 320–480 px without overlap. | +| A10 | P2 | Both Netgraphs | Implemented | Give every phone counter a visible or accessible name; prefer a concise nodes/links summary with optional area/component detail. Validate at 320–480 px without overlap. | | A11 | P2 | Both delivery processes | Planned | Make dependency contributions testable without production credentials, retain trusted provider verification, enforce worldwide exact-revision merge/release checks and enable dependency alerting. M1 covers the runtime lockfile scan and automatic publication of the verified manual-run status on the exact commit; repository enforcement and alerts remain planned. | | A12 | P2 | Worldwide | Planned | Improve setup identity, require an instance-specific MQTT client ID, and refresh upgrade/rollback instructions. A documented install reports its actual revision/version; independent instances have distinct identities. | diff --git a/docs/ui-ux-audit-2026-09-13.md b/docs/ui-ux-audit-2026-09-13.md new file mode 100644 index 0000000..140dc85 --- /dev/null +++ b/docs/ui-ux-audit-2026-09-13.md @@ -0,0 +1,67 @@ +# UI/UX audit — Canada and Worldwide + +Audited baselines: Canada 0.14.1 (`c85ff956`), Worldwide 0.5.0 (`ab5a5836`). + +## Scope and evidence + +Reviewed the Map toolbar, layers, appearance, terrain/buildings, Find, node details, +Live Follow, sound, legends, loading/recovery, and responsive rules; Netgraph's +equivalent controls, counters, selection, and camera interactions; Canada Labs' +experiment, pause, sound, exhibit, and information controls; the Android wrapper's +navigation/recovery source; and Worldwide's installation/provider configuration. + +The Canada public Map and Netgraph were inspected in a browser. In particular, +keyboard activation reproduced Find and Display remaining open together. +Worldwide findings were checked against its corresponding source and are covered +by the paired synthetic Actions suite. No separate Worldwide public instance or +physical Android handset was used for this audit. + +## Findings included in this refinement + +| Priority | Finding and evidence | Change | Acceptance check | +|---|---|---|---| +| P1 | Access to `localStorage` itself can throw before Map startup, and the audio constructor can prevent all three views starting. Existing catches around storage methods do not cover the getter. | A small in-memory fallback protects Map preferences and the audio constructor. | A blocked storage getter still permits Map, Netgraph, and Canada Labs startup. | +| P2 | Appearance, preview, and advanced controls precede the layer grid. At a normal desktop height, common layers extend below the menu's visible area. | Put Traffic and Map details first. Move Terrain/buildings and Appearance into native disclosures; keep the close control at the top while scrolling. | Common traffic toggles fit in the initial panel; expanded controls remain bounded on phones and landscape views. | +| P2 | Layer state relies on a small colored dot. An enabled Buildings layer may show nothing below neighborhood zoom. Camera/shading controls remain active when their layer is off. | Explicit On/Off labels, an enabled-layer count, zoom-aware building guidance, and disabled inactive camera/shading inputs. | Guidance changes at the building zoom threshold; 3D enables Topo/Buildings and their controls correctly. | +| P2 | The existing reset combines layer visibility, scene, style, and camera preferences. | Add Reset layers while retaining a clearly named full reset under Appearance. | Reset layers keeps scene, route preset, and sound preferences; the full reset still restores display defaults. | +| P2 | Netgraph's separate Display controller only coordinates with pointer dismissal. Enter can open Display over Find; Escape does not consistently restore menu focus. | Coordinate menu opening for pointer and keyboard activation and restore focus on dismissal. | Only one menu remains open through Find → Display → Sound; Escape returns to the initiating control. | +| P2 | Netgraph is keyboard-focusable but has no camera keyboard handler. | Arrow-key pan, plus/minus zoom, Home to fit, and an accessible keyboard hint. | Keyboard navigation changes the view without changing topology or intercepting form inputs. | +| P2 | At phone widths, Netgraph hides all counter labels and can crowd the controls row. | Show concise labels and place the summary below the controls. Preserve full labels for assistive technology. | Phone counters are named, visible, and do not overlap controls. | +| P2 | Node details call cumulative retained-link counts “packets,” which can suggest counts within the selected window. Replacing popup contents also recreates its close button without the original accessible name. | Label totals as observations, explain how the window filters links, and retain a named close button after updates. | Live updates keep a usable close control and cumulative totals are identified correctly. | +| P2 | Pausing Follow through its card or opening another control does not always stop an already-running camera ease. | Stop the current follow camera transition for those explicit pause actions. | Pause immediately stops camera motion; the existing ten-second activity dwell remains unchanged. | + +## Further work + +These remain separate, concrete follow-ups; this patch does not claim to complete +every older roadmap item. + +1. **Inspector keyboard continuity:** move focus from search results into details, + preserve the focused neighbour through live reordering, and restore focus to + the originating control on close. The close-button naming defect is addressed + here; the complete focus journey still needs a dedicated regression. +2. **Follow scope and actions:** offer an explicit area lock, Next, and Inspect, + with a clear distinction between heard-at-node activity and confirmed routes. +3. **Motion and text preferences:** evaluate shared reduced-motion and text-size + controls across Map and Netgraph. Labs has a separate artistic presentation; + preserve its Pause and explanation affordances while considering consistency. +4. **Legends and dense selections:** continue testing the combined inspector, + region legend, follow card, and provider notice in short landscape viewports. + Avoid adding permanently visible panels to solve isolated overlaps. +5. **Worldwide first use:** distinguish an empty configured feed, a disconnected + broker, and no routes in the selected window with actionable setup guidance. +6. **Native Android verification:** check system back, TalkBack, large system text, + resume, and offline recovery on hardware. The web improvements reach the + existing wrapper; this patch changes no native navigation policy. + +## Design and validation rules + +Keep one source of truth for appearance and retain existing preferences. Use +native disclosures, stable toggle labels, explicit pressed states, and keyboard +focus restoration, following the [WAI disclosure pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/) +and [button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/). + +Build and test only in GitHub Actions with synthetic traffic. Preserve the existing +privacy, topology, rendering, size, and image checks. Inspect the resulting +desktop/mobile screenshots and verify the exact deployed Canada revision after +the final candidate passes. Runtime evidence is recorded separately from this +source audit. diff --git a/web/e2e/customization.spec.ts b/web/e2e/customization.spec.ts index 108b08c..c1a54c7 100644 --- a/web/e2e/customization.spec.ts +++ b/web/e2e/customization.spec.ts @@ -23,6 +23,7 @@ test('map options are compact, persistent, and preserve live layers through styl await page.locator('.display-advanced > summary').click(); await page.locator('#route-opacity').press('Home'); for (let step = 0; step < 6; step++) await page.locator('#route-opacity').press('ArrowRight'); + if (await page.locator('#hillshade-button').getAttribute('aria-pressed') !== 'true') await page.locator('#hillshade-button').click(); await page.locator('#terrain-relief').press('Home'); for (let step = 0; step < 8; step++) await page.locator('#terrain-relief').press('ArrowRight'); await page.locator('#basemap-style').selectOption('streets'); diff --git a/web/e2e/mapControls.ts b/web/e2e/mapControls.ts index d874e4d..6b505f3 100644 --- a/web/e2e/mapControls.ts +++ b/web/e2e/mapControls.ts @@ -3,4 +3,9 @@ import type { Page } from '@playwright/test'; export async function openMapOptions(page: Page): Promise { const summary = page.locator('#layers-summary'); if (await summary.count() && await summary.getAttribute('aria-expanded') !== 'true') await summary.click(); + // Interaction tests need the complete options; the layer UX test checks the collapsed default. + for (const id of ['terrain-options', 'map-appearance']) { + const group = page.locator('#' + id); + if (await group.count() && await group.getAttribute('open') === null) await group.locator('summary').first().click(); + } } diff --git a/web/e2e/refinements.spec.ts b/web/e2e/refinements.spec.ts index c962e9e..8715217 100644 --- a/web/e2e/refinements.spec.ts +++ b/web/e2e/refinements.spec.ts @@ -109,6 +109,7 @@ test('map navigation and controls remain separate across laptop and narrow phone await expect(page.locator('#status-text')).toBeVisible(); } await page.locator('#layers-summary').click(); + await page.locator('#terrain-options > summary').click(); for (const id of ['routes', 'heatmap', 'clusters', 'hillshade', 'terrain']) { await expect(page.locator(`#${id}-button > span`).last()).toBeVisible(); } diff --git a/web/e2e/tablet-portrait.spec.ts b/web/e2e/tablet-portrait.spec.ts index bdb50d5..cd7a148 100644 --- a/web/e2e/tablet-portrait.spec.ts +++ b/web/e2e/tablet-portrait.spec.ts @@ -41,6 +41,7 @@ test('keeps tablet portrait controls compact, separated, and touch sized', async await summary.click(); await expect(disclosure).toHaveAttribute('open', ''); + await page.locator('#terrain-options > summary').click(); const layerButtons = ['routes', 'heatmap', 'clusters', 'hillshade', 'terrain']; for (const id of layerButtons) await expect(page.locator(`#${id}-button`)).toBeVisible(); await expect(page.locator('#route-window')).toBeVisible(); diff --git a/web/e2e/ui-ux.spec.ts b/web/e2e/ui-ux.spec.ts new file mode 100644 index 0000000..2c79e7c --- /dev/null +++ b/web/e2e/ui-ux.spec.ts @@ -0,0 +1,174 @@ +import { expect, test } from '@playwright/test'; +import { openMapOptions } from './mapControls'; +import { visualFixture, visualPacket } from './visualFixtures'; + +test('puts everyday layers first and keeps close reachable while scrolling options', async ({ page }, info) => { + await visualFixture(page); + await page.goto('/'); + await expect(page.locator('#map')).toHaveAttribute('data-render-state', 'idle'); + await page.locator('#layers-summary').click(); + await expect(page.locator('#map-appearance')).not.toHaveAttribute('open'); + await expect(page.locator('#terrain-options')).not.toHaveAttribute('open'); + await expect(page.locator('#basemap-style')).toBeHidden(); + const panel = await page.locator('#layers-panel').boundingBox(); + for (const id of ['routes-button', 'live-packets-button', 'heatmap-button', 'clusters-button']) { + const box = await page.locator('#' + id).boundingBox(); + expect(box!.y).toBeGreaterThanOrEqual(panel!.y); + expect(box!.y + box!.height).toBeLessThanOrEqual(panel!.y + panel!.height); + } + await expect(page.locator('#layer-summary')).toContainText('layers on'); + await page.screenshot({ path: info.outputPath('layers-first.png') }); + await openMapOptions(page); + await page.locator('.display-advanced > summary').click(); + await page.getByLabel('After-trails', { exact: true }).press('End'); + const close = await page.locator('#layers-close').boundingBox(); + expect(close!.y).toBeGreaterThanOrEqual(panel!.y); + expect(close!.y + close!.height).toBeLessThan(panel!.y + 80); + await page.locator('#layers-close').click(); + await expect(page.locator('#layers-summary')).toBeFocused(); +}); + +test('resets layers without discarding appearance or sound preferences', async ({ page }) => { + await visualFixture(page); + await page.goto('/'); + await expect(page.locator('#map')).toHaveAttribute('data-render-state', 'idle'); + await page.locator('#sound-button').click(); + await page.locator('#sound-volume').press('Home'); + await page.locator('#sound-volume').press('ArrowRight'); + await openMapOptions(page); + await page.locator('#basemap-style').selectOption('streets'); + await page.getByLabel('Route style', { exact: true }).selectOption('neon'); + await page.locator('#terrain-button').click(); + await page.locator('#reset-layers').click(); + await expect(page.locator('#map')).toHaveAttribute('data-basemap-style', 'streets'); + await expect(page.locator('#map')).toHaveAttribute('data-route-preset', 'neon'); + await expect(page.locator('#terrain-button')).toHaveAttribute('aria-pressed', 'false'); + await expect(page.locator('#routes-button')).toHaveAttribute('aria-pressed', 'false'); + await expect(page.locator('#heatmap-button')).toHaveAttribute('aria-pressed', 'true'); + await expect(page.locator('#sound-volume')).toHaveValue('1'); + await expect(page.locator('#sound-button')).toHaveAttribute('aria-pressed', 'false'); + await page.locator('#reset-appearance').click(); + await expect(page.locator('#map')).toHaveAttribute('data-basemap-style', 'dark'); + await expect(page.locator('#map')).toHaveAttribute('data-route-preset', 'crisp'); + await expect(page.locator('#sound-volume')).toHaveValue('1'); +}); + +test('explains zoom-dependent buildings and enables camera controls only in 3D', async ({ page }, info) => { + await visualFixture(page); + await page.goto('/'); + const map = page.locator('#map'); + await expect(map).toHaveAttribute('data-render-state', 'idle'); + await openMapOptions(page); + await page.locator('#buildings-button').click(); + await expect(page.locator('#building-status')).toContainText('Zoom in'); + await expect(page.locator('#camera-pitch')).toBeDisabled(); + await expect(page.locator('#terrain-height')).toBeDisabled(); + await expect(page.locator('#terrain-relief')).toBeDisabled(); + // Exercise the zoom hint without repeatedly loading cold terrain tiles. + // Dedicated terrain tests retain the rendering and responsiveness gates. + await page.keyboard.press('Escape'); + for (let step = 0; step < 3; step++) { + await page.locator('.maplibregl-canvas').press('Equal'); + await expect(map).toHaveAttribute('data-camera-moving', 'false'); + } + await openMapOptions(page); + await expect(page.locator('#building-status')).toHaveText('Building footprints are enabled.'); + await page.locator('#terrain-button').click(); + await expect(page.locator('#hillshade-button')).toHaveAttribute('aria-pressed', 'true'); + await expect(page.locator('#camera-pitch')).toBeEnabled(); + await expect(page.locator('#terrain-height')).toBeEnabled(); + await expect(page.locator('#terrain-relief')).toBeEnabled(); + await expect(page.locator('#building-status')).toHaveText(info.project.name === 'desktop' + ? 'Mapped building heights are shown where available.' : 'Building footprints are enabled.'); +}); + +test('Netgraph menus are exclusive for keyboard users and its camera supports keys', async ({ page }, info) => { + await visualFixture(page); + await page.goto('/netgraph/'); + await expect(page.locator('#connected-count')).toHaveText('2'); + await page.locator('#find-button').press('Enter'); + await page.locator('#display-button').press('Enter'); + await expect(page.locator('#find-panel')).toBeHidden(); + await expect(page.locator('#display-panel')).toBeVisible(); + await page.locator('#sound-button').press('Enter'); + await expect(page.locator('#display-panel')).toBeHidden(); + await expect(page.locator('#sound-panel')).toBeVisible(); + await page.keyboard.press('Escape'); + await expect(page.locator('#sound-button')).toBeFocused(); + if (info.project.name === 'desktop') { + const stage = page.locator('#netgraph-stage'); + const center = await stage.getAttribute('data-view-center'); + const scale = Number(await stage.getAttribute('data-view-scale')); + await stage.press('ArrowRight'); + await expect(stage).not.toHaveAttribute('data-view-center', center!); + await stage.press('Equal'); + await expect.poll(async () => Number(await stage.getAttribute('data-view-scale'))).toBeGreaterThan(scale); + await stage.press('Home'); + await expect(page.locator('#connected-count')).toHaveText('2'); + await page.setViewportSize({ width: 568, height: 320 }); + } + const size = page.viewportSize()!; + if (size.width <= 480 || size.height <= 520) { + for (const label of ['nodes', 'links', 'areas', 'groups']) await expect(page.locator('.summary-compact').filter({ hasText: label })).toBeVisible(); + const summary = await page.locator('#graph-summary').boundingBox(); + const controls = await page.locator('.controls').boundingBox(); + expect(summary!.y).toBeGreaterThanOrEqual(controls!.y + controls!.height); + expect(summary!.y + summary!.height).toBeLessThanOrEqual(size.height); + if (size.width > size.height && size.height <= 520) { + const legend = await page.locator('#legend').boundingBox(); + expect(summary!.x + summary!.width).toBeLessThanOrEqual(legend!.x); + } + } +}); + +test('node details keep a named close button after live updates and explain totals', async ({ page }) => { + await visualFixture(page); + await page.goto('/'); + await expect(page.locator('#map')).toHaveAttribute('data-render-state', 'idle'); + await page.locator('#find-button').click(); + await page.locator('#node-search').fill('Visual Alpha'); + await page.getByRole('option', { name: /Visual Alpha/ }).click(); + await visualPacket(page, 1); + await expect(page.locator('.neighbor-row')).toContainText('total observations'); + await expect(page.locator('.inspector-window-note')).toContainText('earlier observations'); + await page.getByRole('button', { name: 'Close node details', exact: true }).click(); + await expect(page.locator('.node-inspector')).toBeHidden(); +}); + +test('pausing Follow stops a camera transition immediately', async ({ page }, info) => { + test.skip(info.project.name !== 'desktop', 'camera timing is checked once'); + await visualFixture(page, 15); + await page.goto('/'); + const map = page.locator('#map'); + await expect(map).toHaveAttribute('data-render-state', 'idle'); + await page.locator('#follow-button').click(); + await visualPacket(page, 1); + await expect(map).toHaveAttribute('data-camera-moving', 'true'); + await page.locator('#follow-pause').click(); + await expect(map).toHaveAttribute('data-camera-moving', 'false'); + await expect(page.locator('#follow-card')).toHaveAttribute('data-state', 'paused'); +}); + +test('blocked browser storage does not prevent Map, Netgraph, or Labs startup', async ({ page }, info) => { + test.skip(info.project.name !== 'desktop', 'storage behavior does not depend on viewport'); + const errors: string[] = []; + page.on('pageerror', (error) => errors.push(error.message)); + await page.addInitScript(() => Object.defineProperty(window, 'localStorage', { + configurable: true, get() { throw new DOMException('Storage disabled', 'SecurityError'); }, + })); + await page.goto('/'); + await expect(page.locator('#status-text')).not.toHaveText('Starting…'); + await expect(page.locator('#fatal')).toBeHidden(); + const hasLabs = await page.locator('#labs-link').count() > 0; + await page.locator('#layers-summary').click(); + await expect(page.locator('#layers-panel')).toBeVisible(); + await page.goto('/netgraph/'); + await expect(page.locator('#status-text')).not.toHaveText('Starting…'); + await expect(page.locator('#fatal')).toBeHidden(); + if (hasLabs) { + await page.goto('/labs/?demo=1'); + await expect(page.locator('#labs-app')).toHaveAttribute('data-loading', 'false'); + await expect(page.locator('#labs-fatal')).toBeHidden(); + } + expect(errors).toEqual([]); +}); diff --git a/web/e2e/visual-smoke.spec.ts b/web/e2e/visual-smoke.spec.ts index ddb040b..7f06de3 100644 --- a/web/e2e/visual-smoke.spec.ts +++ b/web/e2e/visual-smoke.spec.ts @@ -526,7 +526,7 @@ test('focuses recent route neighbors and clears selection on the map', async ({ await expect(map).toHaveAttribute('data-hovered-route-id', 'a-b'); await expect(tooltip).toHaveAttribute('data-kind', 'route'); await expect(tooltip).toContainText('Alpha ↔ Bravo'); - await expect(tooltip).toContainText('12 packets'); + await expect(tooltip).toContainText('12 total observations'); await expectTooltipInsideViewport(tooltip, page); await page.screenshot({ path: testInfo.outputPath(`cartolite-neighbors-focus-${testInfo.project.name}.png`) }); diff --git a/web/index.html b/web/index.html index 3afd2f4..19d586a 100644 --- a/web/index.html +++ b/web/index.html @@ -55,15 +55,14 @@ - Map + Layers -
diff --git a/web/netgraph/index.html b/web/netgraph/index.html index 3a1c68c..f26e092 100644 --- a/web/netgraph/index.html +++ b/web/netgraph/index.html @@ -13,7 +13,7 @@
-
+
@@ -67,16 +67,17 @@
+

Use arrow keys to pan, plus and minus to zoom, and Home to fit the topology.