mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(web): rebuild srcdoc image export after wrap failure
This commit is contained in:
parent
e852bee4ec
commit
d7a2ae8a69
2 changed files with 126 additions and 17 deletions
|
|
@ -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,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue