From 7b1e4abcd6286d7239069bbcf7eb1c1bc583a421 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 05:19:01 +0000 Subject: [PATCH] feat: support null speed from geolocation Treats explicit `null` speed updates from Geolocation API as valid updates, transitioning the UI to the placeholder state instead of preserving the last known speed. This ensures that when the device reports it cannot calculate speed (but is active), the UI reflects this state accurately. - Modified `src/app.ts` to accept `speed: null` in `handlePosition`. - Added E2E regression test `tests/e2e/null_speed.spec.ts` mocking geolocation updates. --- package-lock.json | 4 +- package.json | 2 +- src/app.ts | 16 +++++-- tests/e2e/null_speed.spec.ts | 86 ++++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 tests/e2e/null_speed.spec.ts diff --git a/package-lock.json b/package-lock.json index aa64357..88e9b54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "speedometer", - "version": "0.0.84", + "version": "0.0.85", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "speedometer", - "version": "0.0.84", + "version": "0.0.85", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index dd00a77..3baac24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "speedometer", - "version": "0.0.84", + "version": "0.0.85", "description": "Minimal PWA speedometer that displays GPS speed. Includes TypeScript script to render PNG icons from SVG using sharp.", "license": "MIT", "private": true, diff --git a/src/app.ts b/src/app.ts index d10999d..bdcfe82 100644 --- a/src/app.ts +++ b/src/app.ts @@ -266,14 +266,24 @@ function handlePosition(pos: GeolocationPosition): void { const { speed, accuracy } = pos.coords; // Update speed only when native speed is provided and valid - if (typeof speed === "number" && Number.isFinite(speed) && speed >= 0) { + // Treat "null" as a valid value (placeholder), regardless of previous state + const isValidSpeed = typeof speed === "number" && Number.isFinite(speed) && speed >= 0; + const isNullSpeed = speed === null; + + if (isValidSpeed || isNullSpeed) { const now = Date.now(); - lastSpeedMs = speed; - renderSpeed(speed); lastUpdateTimestamp = now; if (warningEl) { warningEl.hidden = true; } + + if (isValidSpeed) { + lastSpeedMs = speed; + renderSpeed(speed!); + } else { + lastSpeedMs = null; + showPlaceholder(); + } } // Status/accuracy diff --git a/tests/e2e/null_speed.spec.ts b/tests/e2e/null_speed.spec.ts new file mode 100644 index 0000000..d96b0f4 --- /dev/null +++ b/tests/e2e/null_speed.spec.ts @@ -0,0 +1,86 @@ +import { expect, test } from "@playwright/test"; + +test.describe("Speedometer Null Speed Handling", () => { + test.beforeEach(async ({ page, context }) => { + await context.grantPermissions(["geolocation"]); + + // Inject mocks BEFORE the page loads scripts to pass startup checks + await page.addInitScript(() => { + // Enable Test Mode to bypass strict device checks + (window as any).__TEST_MODE__ = true; + + // Mock Geolocation + (window as any).__geoSuccessCallback = null; + + const mockGeolocation = { + watchPosition: (success: any) => { + (window as any).__geoSuccessCallback = success; + return 123; + }, + clearWatch: () => {}, + getCurrentPosition: () => {}, + }; + + try { + Object.defineProperty(navigator, "geolocation", { + value: mockGeolocation, + configurable: true, + }); + } catch (e) { + console.error("Failed to mock navigator.geolocation:", e); + } + + // Clear storage + localStorage.clear(); + localStorage.setItem("info-popover-shown", "true"); + }); + + await page.goto("/"); + }); + + test("Updates to placeholder when speed becomes null", async ({ page }) => { + // Wait for watchPosition to be called + await page.waitForFunction(() => (window as any).__geoSuccessCallback !== null, null, { timeout: 5000 }); + + // 1. Send valid speed (10 m/s approx 22 mph) + await page.evaluate(() => { + const position = { + coords: { + speed: 10, + accuracy: 5, + latitude: 0, + longitude: 0, + altitude: null, + altitudeAccuracy: null, + heading: null, + }, + timestamp: Date.now(), + }; + (window as any).__geoSuccessCallback(position); + }); + + const speedEl = page.locator("#speed"); + await expect(speedEl).not.toHaveText("———"); + await expect(speedEl).not.toHaveText("0"); // 10 m/s is not 0 + + // 2. Send null speed + await page.evaluate(() => { + const position = { + coords: { + speed: null, + accuracy: 10, + latitude: 0, + longitude: 0, + altitude: null, + altitudeAccuracy: null, + heading: null, + }, + timestamp: Date.now(), + }; + (window as any).__geoSuccessCallback(position); + }); + + // 3. Verify it shows placeholder + await expect(speedEl).toHaveText("———", { timeout: 2000 }); + }); +});