diff --git a/client/interface/Interface.tsx b/client/interface/Interface.tsx index 663c757..e1a53d6 100644 --- a/client/interface/Interface.tsx +++ b/client/interface/Interface.tsx @@ -1972,10 +1972,12 @@ export const GamepadInterface = () => { active={activeControls.a} onMouseDown={() => handleControlPress("Enter")} onMouseUp={() => handleControlRelease("Enter")} - disabled={ - isOffline || - isGeneratingAt(getCurrentPath()[currentDepth]?.id) - } + // Offline status is advisory: navigator.onLine is not reliable + // enough to disable every A-button action (including local New + // Story), and generation will surface a real fetch failure. + disabled={isGeneratingAt( + getCurrentPath()[currentDepth]?.id, + )} /> diff --git a/client/interface/hooks/useStoryTree.ts b/client/interface/hooks/useStoryTree.ts index 6c4b34b..770030d 100644 --- a/client/interface/hooks/useStoryTree.ts +++ b/client/interface/hooks/useStoryTree.ts @@ -445,7 +445,6 @@ export function useStoryTree(params: StoryParams) { } return false; case "Enter": { - if (error) return false; const loom = loomsById[currentLoomId]; if (!loom) return false; @@ -532,7 +531,6 @@ export function useStoryTree(params: StoryParams) { return true; }, [ - error, getCurrentPath, getOptionsAtDepth, currentDepth, diff --git a/client/interface/hooks/useTextGeneration.ts b/client/interface/hooks/useTextGeneration.ts index 59333a1..1422a3f 100644 --- a/client/interface/hooks/useTextGeneration.ts +++ b/client/interface/hooks/useTextGeneration.ts @@ -56,15 +56,11 @@ export function useTextGeneration() { ) => { setError(null); - // Check if we're offline - if (!navigator.onLine) { - const offlineMessage = - "No internet connection - generation requires online access"; - setError({ message: offlineMessage }); - throw new Error(offlineMessage); - } - try { + // Do not gate the request on navigator.onLine. Browsers and installed + // PWAs can report a stale offline value even when this origin is + // reachable. The fetch itself is the authoritative connectivity check, + // and its failure is translated into the visible network error below. const response = await fetch("/api/generate", { method: "POST", headers: { diff --git a/tests/e2e/offline-controls.e2e.ts b/tests/e2e/offline-controls.e2e.ts new file mode 100644 index 0000000..4e361d0 --- /dev/null +++ b/tests/e2e/offline-controls.e2e.ts @@ -0,0 +1,47 @@ +import { expect, test, type Page } from "@playwright/test"; + +async function waitForStory(page: Page) { + await expect(page.locator(".gamepad-main")).toHaveAttribute( + "data-story-ready", + "true", + ); +} + +test("offline advice never disables local choices or a later generation retry", async ({ + page, + context, +}) => { + await page.goto("/"); + await waitForStory(page); + await context.setOffline(true); + + // Rise from the root to the floor and create a local loom using the physical + // controls. This used to be impossible because offline status disabled A + // globally, even though creating a story needs no network. + await page.keyboard.press("ArrowUp"); + await expect(page.locator(".mode-bar-title")).toHaveText("LOOMS"); + await page.getByRole("button", { name: "Select button" }).click(); + await expect(page.locator(".action-sheet-title")).toHaveText("FLOOR"); + await page.getByRole("button", { name: "A button" }).click(); + await expect(page.locator(".mode-bar-title")).toHaveText("LOOM"); + + // A real failed request is visible, but it must not latch generation off. + await page.getByRole("button", { name: "A button" }).click(); + await expect(page.locator(".navigation-bar")).toContainText("Network error"); + + await context.setOffline(false); + await page.route("**/api/generate", async (route) => { + await route.fulfill({ + status: 200, + contentType: "text/event-stream", + body: 'data: {"content":" The connection returned."}\n\ndata: [DONE]\n\n', + }); + }); + await page.getByRole("button", { name: "A button" }).click(); + await expect(page.locator(".navigation-bar")).not.toContainText( + "Network error", + ); + await expect(page.locator(".story-text")).toContainText( + "The connection returned.", + ); +});