fix(web): rebuild srcdoc image export after wrap failure

This commit is contained in:
bulai0408 2026-05-31 06:18:02 +08:00
parent e852bee4ec
commit d7a2ae8a69
2 changed files with 126 additions and 17 deletions

View file

@ -4668,19 +4668,31 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
};
}, [source, effectiveDeck, projectId, file.name, useUrlLoadPreview]);
const buildPreviewSrcDoc = useCallback(() => {
if (!previewSource) return '';
return buildSrcdoc(previewSource, {
deck: effectiveDeck,
baseHref: projectRawUrl(projectId, baseDirFor(file.name)),
initialSlideIndex: htmlPreviewSlideState.get(previewStateKey)?.active ?? 0,
selectionBridge: true,
editBridge: manualEditMode,
paletteBridge: false,
previewFocusGuard: true,
});
}, [
previewSource,
effectiveDeck,
projectId,
file.name,
previewStateKey,
manualEditMode,
]);
const srcDoc = useMemo(() => {
if (!previewSource) return '';
if (srcDocWrapFailure === previewSource) return SRC_DOC_PREVIEW_WRAP_FAILURE_PLACEHOLDER;
try {
return buildSrcdoc(previewSource, {
deck: effectiveDeck,
baseHref: projectRawUrl(projectId, baseDirFor(file.name)),
initialSlideIndex: htmlPreviewSlideState.get(previewStateKey)?.active ?? 0,
selectionBridge: true,
editBridge: manualEditMode,
paletteBridge: false,
previewFocusGuard: true,
});
return buildPreviewSrcDoc();
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
console.warn(
@ -4704,11 +4716,8 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
activePreviewSrcUrl,
previewSource,
srcDocWrapFailure,
effectiveDeck,
projectId,
buildPreviewSrcDoc,
file.name,
previewStateKey,
manualEditMode,
clearSrcDocOnlyPreviewModes,
]);
const lazySrcDocTransport = useMemo(() => buildLazySrcdocTransport(), []);
@ -4808,11 +4817,14 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
run(target);
return true;
}, []);
const activateSrcDocSnapshotTransport = useCallback((target: HTMLIFrameElement | null = srcDocPreviewIframeRef.current) => {
if (!srcDoc) return false;
const activateSrcDocSnapshotTransport = useCallback((
target: HTMLIFrameElement | null = srcDocPreviewIframeRef.current,
html = srcDoc,
) => {
if (!html || html === SRC_DOC_PREVIEW_WRAP_FAILURE_PLACEHOLDER) return false;
const win = target?.contentWindow;
if (!win) return false;
win.postMessage({ type: 'od:srcdoc-transport-activate', html: srcDoc }, '*');
win.postMessage({ type: 'od:srcdoc-transport-activate', html }, '*');
return true;
}, [srcDoc]);
useEffect(() => {
@ -6415,17 +6427,33 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
return activeIframe ? requestPreviewSnapshot(activeIframe) : null;
}
let snapshotSrcDoc = srcDoc;
if (srcDocWrapFailure === previewSource || snapshotSrcDoc === SRC_DOC_PREVIEW_WRAP_FAILURE_PLACEHOLDER) {
if (!previewSource) return null;
try {
snapshotSrcDoc = buildPreviewSrcDoc();
setSrcDocWrapFailure((current) => (current === previewSource ? null : current));
} catch (err) {
console.warn('[exportAsImage] failed to rebuild srcdoc snapshot after wrap failure:', err);
return null;
}
}
if (!srcDocShellReady) {
await waitForIframeLoadOrTimeout(srcDocIframe, 500);
}
const activated = activateSrcDocSnapshotTransport(srcDocIframe);
const activated = activateSrcDocSnapshotTransport(srcDocIframe, snapshotSrcDoc);
if (activated) {
await waitForIframeLoadOrTimeout(srcDocIframe);
}
return requestPreviewSnapshot(srcDocIframe);
}, [
activateSrcDocSnapshotTransport,
buildPreviewSrcDoc,
previewSource,
srcDoc,
srcDocShellReady,
srcDocWrapFailure,
useUrlLoadPreview,
]);

View file

@ -1168,6 +1168,87 @@ describe('FileViewer SVG artifacts', () => {
expect((screen.getByTestId('artifact-preview-frame') as HTMLIFrameElement).getAttribute('data-od-render-mode')).toBe('url-load');
});
it('rebuilds srcdoc for Share -> Export Image after a transient wrap failure', async () => {
const actualSrcdoc = await vi.importActual<typeof import('../../src/runtime/srcdoc')>(
'../../src/runtime/srcdoc',
);
const file = baseFile({
name: 'deck.html',
path: 'deck.html',
mime: 'text/html',
kind: 'html',
artifactManifest: {
version: 1,
kind: 'deck',
title: 'Deck',
entry: 'deck.html',
renderer: 'deck-html',
exports: ['html'],
},
});
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
requestPreviewSnapshotMock.mockResolvedValue({
dataUrl: 'data:image/png;base64,AAAA',
w: 100,
h: 80,
});
function Host() {
const [html, setHtml] = useState(
'<html><body><section class="slide">Initial slide</section></body></html>',
);
return (
<>
<button
type="button"
data-testid="bump-source"
onClick={() => setHtml('<html><body><section class="slide">Recovered slide</section></body></html>')}
>
bump source
</button>
<FileViewer
projectId="project-1"
projectKind="prototype"
file={file}
isDeck
liveHtml={html}
/>
</>
);
}
render(<Host />);
expect((screen.getByTestId('artifact-preview-frame') as HTMLIFrameElement).getAttribute('data-od-render-mode')).toBe('url-load');
const srcdocFrame = screen.getByTestId('artifact-preview-frame-srcdoc') as HTMLIFrameElement;
const postMessageSpy = vi.spyOn(srcdocFrame.contentWindow!, 'postMessage');
fireEvent.load(srcdocFrame);
buildSrcdocMock.mockImplementation(() => {
throw new Error('wrap failed once');
});
fireEvent.click(screen.getByTestId('bump-source'));
await waitFor(() => {
expect(warnSpy).toHaveBeenCalledWith(
'open-design preview fallback: srcdoc iframe failed; loading raw URL instead',
expect.objectContaining({
stage: 'wrap',
}),
);
});
buildSrcdocMock.mockImplementation(actualSrcdoc.buildSrcdoc);
fireEvent.click(screen.getByRole('button', { name: /share/i }));
fireEvent.click(await screen.findByTestId('share-menu-export-image'));
await waitFor(() => {
expect(requestPreviewSnapshotMock).toHaveBeenCalledWith(srcdocFrame);
});
const activations = srcDocActivationMessages(postMessageSpy.mock.calls);
expect(activations.at(-1)?.html).toContain('Recovered slide');
expect(activations.at(-1)?.html).not.toBe('<!doctype html><html><body></body></html>');
});
it('falls back to URL-load when srcdoc transport activation reports a write failure', async () => {
const file = baseFile({
name: 'deck.html',