fix(web): render srcdoc artifacts directly after leaving URL-load (#3042)

Lazy srcdoc transport was still active after URL-load preview switched off,
leaving the visible iframe on an empty activation shell until Edit forced a
full srcdoc reload. Mount real artifact HTML whenever srcdoc is the active
transport and remount when leaving URL-load.

Fixes #2791
This commit is contained in:
吴杨帆 2026-05-27 14:21:01 +08:00 committed by GitHub
parent 3d6e06ad21
commit 4808cdab3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 44 additions and 10 deletions

View file

@ -4352,13 +4352,9 @@ function HtmlViewer({
[previewSource, effectiveDeck, projectId, file.name, previewStateKey, manualEditMode],
);
const lazySrcDocTransport = useMemo(() => buildLazySrcdocTransport(), []);
const [hasLazySrcDocTransport, setHasLazySrcDocTransport] = useState(useUrlLoadPreview);
const [srcDocTransportResetKey, setSrcDocTransportResetKey] = useState(0);
const [srcDocShellReady, setSrcDocShellReady] = useState(false);
const wasUrlLoadPreviewRef = useRef(useUrlLoadPreview);
useEffect(() => {
if (useUrlLoadPreview) setHasLazySrcDocTransport(true);
}, [useUrlLoadPreview]);
// Reset the shell-ready latch whenever the srcDoc iframe re-mounts. The
// next shell will post `od:srcdoc-transport-ready` (or fire onLoad) and
// flip this back to true. See #2253.
@ -4380,7 +4376,11 @@ function HtmlViewer({
window.addEventListener('message', onMessage);
return () => window.removeEventListener('message', onMessage);
}, []);
const useLazySrcDocTransport = !manualEditMode && (useUrlLoadPreview || hasLazySrcDocTransport);
// Lazy transport preloads an empty shell only while URL-load is the active
// transport. Once srcdoc becomes active (sandbox shim, Draw, Tweaks, etc.),
// mount the real artifact HTML directly so we do not depend on a postMessage
// activation that can race (#2253) and strand the iframe blank (#2361, #2791).
const useLazySrcDocTransport = !manualEditMode && useUrlLoadPreview;
const srcDocTransportContent = useLazySrcDocTransport ? lazySrcDocTransport : srcDoc;
const urlTransportSrc = useUrlLoadPreview ? activePreviewSrcUrl : 'about:blank';
const activateSrcDocTransport = useCallback((target: HTMLIFrameElement | null = srcDocPreviewIframeRef.current) => {
@ -4438,6 +4438,10 @@ function HtmlViewer({
wasUrlLoadPreviewRef.current = true;
return;
}
if (wasUrlLoadPreviewRef.current) {
setSrcDocTransportResetKey((key) => key + 1);
activatedSrcDocTransportHtmlRef.current = null;
}
wasUrlLoadPreviewRef.current = false;
activateSrcDocTransport();
}, [activateSrcDocTransport, useUrlLoadPreview]);

View file

@ -454,7 +454,6 @@ describe('FileViewer SVG artifacts', () => {
const srcDocFrameAfter = container.querySelector('iframe[data-od-render-mode="srcdoc"]') as HTMLIFrameElement | null;
expect(urlFrameAfter).toBe(urlFrame);
expect(srcDocFrameAfter).toBe(srcDocFrame);
expect(urlFrameAfter?.getAttribute('data-od-active')).toBe('false');
expect(urlFrameAfter?.getAttribute('src')).toBe('about:blank');
expect(srcDocFrameAfter?.getAttribute('data-od-active')).toBe('true');
@ -462,6 +461,38 @@ describe('FileViewer SVG artifacts', () => {
expect(srcDocFrameAfter?.srcdoc).toContain('data-od-edit-bridge');
});
it('renders sandbox-shim artifacts on the srcdoc transport without entering edit mode (#2791)', () => {
const file = baseFile({
name: 'search.html',
path: 'search.html',
mime: 'text/html',
kind: 'html',
artifactManifest: {
version: 1,
kind: 'html',
title: 'Search',
entry: 'search.html',
renderer: 'html',
exports: ['html'],
},
});
const { container } = render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={file}
liveHtml='<html><body><script src="app.js"></script><main data-od-id="results">Results</main></body></html>'
/>,
);
const srcDocFrame = container.querySelector('iframe[data-od-render-mode="srcdoc"]') as HTMLIFrameElement | null;
expect(srcDocFrame?.getAttribute('data-od-active')).toBe('true');
expect(srcDocFrame?.srcdoc).toContain('data-od-id="results"');
expect(srcDocFrame?.srcdoc).not.toContain('data-od-lazy-srcdoc-transport');
expect(srcDocFrame?.srcdoc).toContain('data-od-sandbox-shim');
});
it('reactivates the srcDoc transport after switching source back to preview', async () => {
const file = baseFile({
name: 'page.html',
@ -1498,18 +1529,17 @@ describe('FileViewer tweaks toolbar', () => {
);
expect((screen.getByTestId('artifact-preview-frame') as HTMLIFrameElement).getAttribute('data-od-render-mode')).toBe('url-load');
const inactiveSrcDocFrame = screen.getByTestId('artifact-preview-frame-srcdoc') as HTMLIFrameElement;
const postMessageSpy = vi.spyOn(inactiveSrcDocFrame.contentWindow!, 'postMessage');
clickAgentTool('draw-overlay-toggle');
const frame = await waitFor(() => {
const activeFrame = screen.getByTestId('artifact-preview-frame') as HTMLIFrameElement;
expect(activeFrame.getAttribute('data-od-render-mode')).toBe('srcdoc');
expect(activeFrame.srcdoc).toContain('data-od-lazy-srcdoc-transport');
expect(activeFrame.srcdoc).toContain('data-od-selection-bridge');
expect(activeFrame.srcdoc).not.toContain('data-od-lazy-srcdoc-transport');
return activeFrame;
});
await waitFor(() => {
expect(srcDocActivationMessages(postMessageSpy.mock.calls).at(-1)?.html).toContain('data-od-selection-bridge');
expect(frame.srcdoc).toContain('data-od-id="hero"');
});
expect(screen.queryByRole('button', { name: 'Click' })).toBeNull();
expect(screen.getByRole('button', { name: 'Undo' })).toBeTruthy();