Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions client/interface/Interface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)}
/>
</div>
</div>
Expand Down
2 changes: 0 additions & 2 deletions client/interface/hooks/useStoryTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -532,7 +531,6 @@ export function useStoryTree(params: StoryParams) {
return true;
},
[
error,
getCurrentPath,
getOptionsAtDepth,
currentDepth,
Expand Down
12 changes: 4 additions & 8 deletions client/interface/hooks/useTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
47 changes: 47 additions & 0 deletions tests/e2e/offline-controls.e2e.ts
Original file line number Diff line number Diff line change
@@ -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.",
);
});