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 }); + }); +});