Unverified 提交 92b90720 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Fix preserved URL not cleared when navigating to root or restarting (#2422)

## Summary - Fix regression from PR #2336 where `previewCurrentUrlAtom` wasn't cleared when navigating back to root (`/`) - Clear preserved URL in `pushState`/`replaceState` handlers when pathname is "/" or empty - Clear preserved URL in `restartApp` before restarting to prevent stale route restoration - Add E2E test to verify route stays on root after restart Fixes the issue where HMR/restart would load the wrong URL after navigating back to root from a sub-route. ## Test plan - [x] E2E test `restart after navigating back to root should stay on root` passes - [x] Existing test `refresh preserves current route` still passes - [x] Unit tests pass (661 tests) 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2422"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes preview navigation state persistence and restart behavior, which could impact route restoration and back/forward history across apps. Added e2e coverage reduces the chance of regressions. > > **Overview** > Fixes a regression where the preview could restore a stale sub-route after returning to `/` or after an app restart. > > `PreviewIframe` now **clears `previewCurrentUrlAtom`** when `pushState`/`replaceState` navigates to same-origin root, and `useRunApp.restartApp` clears the preserved URL for the app before restarting to avoid remount restoring the wrong route. > > Adds an e2e Playwright test ensuring navigating `/about` → `/` and then restarting stays on the home route. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 16db90a34c728f311599eab4c029010d89354420. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Fixes a regression where the preview restored a stale sub-route after returning to “/” or restarting. We now clear the preserved URL so the app stays on the expected root route. - **Bug Fixes** - Clear preserved URL on pushState/replaceState when pathname is “/” in PreviewIframe. - Clear preserved URL during restart (useRunApp) to prevent stale route restoration. - Add E2E test to ensure restart after returning to root stays on “/”. - **New Features** - Allow disabling the stop hook via DISABLE_DYAD_STOP_HOOK environment variable. <sup>Written for commit 16db90a34c728f311599eab4c029010d89354420. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 eb909bb0
......@@ -21,6 +21,10 @@ import subprocess
import sys
from pathlib import Path
# Allow disabling this hook via environment variable
if os.environ.get("DISABLE_DYAD_STOP_HOOK", "").lower() in ("true", "1", "yes"):
sys.exit(0)
def extract_task_tool_calls(transcript_path: str) -> list[dict]:
"""Extract all Task* tool calls from the transcript.
......
import { testSkipIfWindows, Timeout } from "./helpers/test_helper";
import { expect } from "@playwright/test";
import * as fs from "fs";
import * as path from "path";
// This test reproduces a regression from PR #2336 where navigating back to root
// doesn't clear the preserved URL, causing the wrong route to load after HMR
testSkipIfWindows(
"HMR after navigating back to root should stay on root",
async ({ po }) => {
await po.setUp({ autoApprove: true });
// Create a multi-page app with react-router navigation
await po.sendPrompt("tc=multi-page");
// Wait for the preview iframe to be visible and loaded
await po.expectPreviewIframeIsVisible();
// Wait for the Home Page content to be visible in the iframe
await expect(
po
.getPreviewIframeElement()
.contentFrame()
.getByRole("heading", { name: "Home Page" }),
).toBeVisible({ timeout: Timeout.LONG });
// Navigate to /about by clicking the link
await po
.getPreviewIframeElement()
.contentFrame()
.getByText("Go to About Page")
.click();
// Wait for About Page to be visible
await expect(
po
.getPreviewIframeElement()
.contentFrame()
.getByRole("heading", { name: "About Page" }),
).toBeVisible({ timeout: Timeout.MEDIUM });
// Navigate back to / by clicking the link (triggers pushState with pathname "/")
// This is the scenario that triggers the bug - pushState to "/" doesn't clear preserved URL
await po
.getPreviewIframeElement()
.contentFrame()
.getByText("Go to Home Page")
.click();
// Wait for Home Page to be visible
await expect(
po
.getPreviewIframeElement()
.contentFrame()
.getByRole("heading", { name: "Home Page" }),
).toBeVisible({ timeout: Timeout.MEDIUM });
// Verify address bar shows root path
await expect(po.page.getByTestId("preview-address-bar-path")).toHaveText(
"/",
);
// Get the app path to modify the Index.tsx file
const appPath = await po.getCurrentAppPath();
if (!appPath) {
throw new Error("No app path found");
}
// Trigger HMR by modifying the Index.tsx file
const indexPath = path.join(appPath, "src/pages/Index.tsx");
const originalContent = fs.readFileSync(indexPath, "utf-8");
// Add a comment to trigger HMR without changing behavior
const modifiedContent = originalContent.replace(
"<h1",
"{/* HMR trigger */}\n <h1",
);
fs.writeFileSync(indexPath, modifiedContent);
// Wait for HMR to complete - the page should reload but stay on root
// Give time for the file watcher and HMR to process
await po.page.waitForTimeout(2000);
// After HMR, the page should still be on Home Page (/)
// BUG: Due to the regression, it might incorrectly load /about
await expect(
po
.getPreviewIframeElement()
.contentFrame()
.getByRole("heading", { name: "Home Page" }),
).toBeVisible({ timeout: Timeout.MEDIUM });
await expect(
po
.getPreviewIframeElement()
.contentFrame()
.getByRole("heading", { name: "About Page" }),
).not.toBeVisible();
},
);
......@@ -643,6 +643,13 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
...prev,
[selectedAppId]: urlToPreserve,
}));
} else if (newUrlObj.origin === appUrlObj.origin) {
// Clear preserved URL when navigating back to root
setPreservedUrls((prev) => {
const next = { ...prev };
delete next[selectedAppId];
return next;
});
}
} catch {
// Invalid URL, don't preserve
......@@ -671,6 +678,13 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
...prev,
[selectedAppId]: urlToPreserve,
}));
} else if (newUrlObj.origin === appUrlObj.origin) {
// Clear preserved URL when navigating back to root
setPreservedUrls((prev) => {
const next = { ...prev };
delete next[selectedAppId];
return next;
});
}
} catch {
// Invalid URL, don't preserve
......
......@@ -7,6 +7,7 @@ import {
currentAppAtom,
previewPanelKeyAtom,
previewErrorMessageAtom,
previewCurrentUrlAtom,
selectedAppIdAtom,
} from "@/atoms/appAtoms";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
......@@ -127,6 +128,7 @@ export function useRunApp() {
const setConsoleEntries = useSetAtom(appConsoleEntriesAtom);
const [, setAppUrlObj] = useAtom(appUrlAtom);
const setPreviewPanelKey = useSetAtom(previewPanelKeyAtom);
const setPreservedUrls = useSetAtom(previewCurrentUrlAtom);
const appId = useAtomValue(selectedAppIdAtom);
const setPreviewErrorMessage = useSetAtom(previewErrorMessageAtom);
......@@ -218,6 +220,13 @@ export function useRunApp() {
// Clear the URL and add restart message
setAppUrlObj({ appUrl: null, appId: null, originalUrl: null });
// Clear preserved URL to prevent stale route restoration after restart
setPreservedUrls((prev) => {
const next = { ...prev };
delete next[appId];
return next;
});
// Clear logs in both the backend store and UI state
await ipc.misc.clearLogs({ appId });
setConsoleEntries([]);
......@@ -254,7 +263,14 @@ export function useRunApp() {
setLoading(false);
}
},
[appId, setApp, setConsoleEntries, setAppUrlObj, setPreviewPanelKey],
[
appId,
setApp,
setConsoleEntries,
setAppUrlObj,
setPreviewPanelKey,
setPreservedUrls,
],
);
const refreshAppIframe = useCallback(async () => {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论