Unverified 提交 fb73c208 authored 作者: wwwillchen-bot's avatar wwwillchen-bot 提交者: GitHub

Fix preview navigation black screen when selecting routes from dropdown (#2610)

## Summary - Changed `navigateToRoute()` to use `postMessage` + `location.replace()` pattern instead of direct `location.href` assignment - This matches the approach used by back/forward navigation buttons and provides smooth navigation without the black screen flicker and sidebar state reset - Also added proper `currentIframeUrlRef` and `preservedUrls` updates for HMR remount consistency Fixes #2428 ## Test plan - [x] Build succeeds - [x] Unit tests pass (784 tests) - [x] E2E tests pass for preview navigation (`npm run e2e -- --grep "preview navigation"`) - Manual testing: Open a multi-page app, use the route dropdown to navigate between routes - should no longer see black screen flicker or sidebar collapse 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2610" target="_blank"> <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 --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Fixes #2428: removes black screen flicker and sidebar collapse when selecting routes from the preview dropdown. Navigation now uses postMessage + location.replace and keeps the iframe src stable across SPA navigation and HMR. - **Bug Fixes** - Use postMessage + location.replace to navigate; prevents flicker and preserves sidebar state. - Freeze iframe src with useMemo and same-origin check so SPA nav/HMR don’t reset it; added e2e test to verify src remains unchanged. - Sync navigation state: history, canGoBack/canGoForward, currentIframeUrlRef, and per-app preservedUrls (clears on root). <sup>Written for commit 1bdd7635ea432c564fe77fea57062df1a9d2f56d. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarWill Chen <willchen90@gmail.com> Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 8f87e0f3
......@@ -153,3 +153,28 @@ testSkipIfWindows(
).toBeVisible({ timeout: Timeout.MEDIUM });
},
);
testSkipIfWindows(
"spa navigation inside iframe does not change iframe src attribute",
async ({ po }) => {
await po.setUp({ autoApprove: true });
await po.sendPrompt("tc=multi-page");
await po.previewPanel.expectPreviewIframeIsVisible();
const iframe = po.previewPanel.getPreviewIframeElement();
await expect(
iframe.contentFrame().getByRole("heading", { name: "Home Page" }),
).toBeVisible({ timeout: Timeout.LONG });
const srcBeforeNavigation = await iframe.getAttribute("src");
await iframe.contentFrame().getByText("Go to About Page").click();
await expect(
iframe.contentFrame().getByRole("heading", { name: "About Page" }),
).toBeVisible({ timeout: Timeout.MEDIUM });
const srcAfterNavigation = await iframe.getAttribute("src");
expect(srcAfterNavigation).toBe(srcBeforeNavigation);
},
);
......@@ -6,7 +6,7 @@ import {
previewCurrentUrlAtom,
} from "@/atoms/appAtoms";
import { useAtomValue, useSetAtom, useAtom } from "jotai";
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import {
ArrowLeft,
ArrowRight,
......@@ -936,10 +936,15 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
const baseUrl = new URL(appUrl).origin;
const newUrl = `${baseUrl}${path}`;
// Navigate to the URL
iframeRef.current.contentWindow.location.href = newUrl;
// iframeRef.current.src = newUrl;
// Use postMessage to navigate (same as back/forward) - this uses location.replace()
// which provides smooth navigation without the black screen flicker that location.href causes
iframeRef.current.contentWindow.postMessage(
{
type: "navigate",
payload: { url: newUrl },
},
"*",
);
// Update navigation history
const newHistory = [
......@@ -950,9 +955,50 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
setCurrentHistoryPosition(newHistory.length - 1);
setCanGoBack(true);
setCanGoForward(false);
// Update iframe URL ref to match
currentIframeUrlRef.current = newUrl;
// Update preservedUrls to match navigation (for HMR remounts)
if (selectedAppId) {
// Clear preserved URL if navigating to root, otherwise update it
if (path === "/" || path === "") {
setPreservedUrls((prev) => {
const newUrls = { ...prev };
delete newUrls[selectedAppId];
return newUrls;
});
} else {
setPreservedUrls((prev) => ({
...prev,
[selectedAppId]: newUrl,
}));
}
}
}
};
// Freeze iframe src between remounts so in-iframe SPA navigation (pushState/replaceState)
// doesn't cause React to set a new src and trigger a second full navigation flicker.
const iframeSrc = useMemo(() => {
if (!appUrl) {
return undefined;
}
const currentUrl = currentIframeUrlRef.current;
if (!currentUrl) {
return appUrl;
}
try {
const currentOrigin = new URL(currentUrl).origin;
const appOrigin = new URL(appUrl).origin;
return currentOrigin === appOrigin ? currentUrl : appUrl;
} catch {
return appUrl;
}
}, [appUrl, reloadKey, selectedAppId]);
// Display loading state
if (loading) {
return (
......@@ -984,9 +1030,6 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
restartApp();
};
// Convert null to undefined for iframe src prop compatibility
const iframeSrc = currentIframeUrlRef.current ?? appUrl ?? undefined;
return (
<div className="flex flex-col h-full">
{/* Browser-style header - hide when annotator is active */}
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论