From 2fee76ac569d9224e21522aed004bbebd72ad8de Mon Sep 17 00:00:00 2001 From: xy200303 <3483421977@qq.com> Date: Fri, 5 Jun 2026 13:59:51 +0800 Subject: [PATCH 1/2] fix(super-editor): rethrow export docx errors --- .../src/editors/v1/core/Editor.ts | 5 ++- .../v1/tests/export/exportDocx.errors.test.js | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 packages/super-editor/src/editors/v1/tests/export/exportDocx.errors.test.js diff --git a/packages/super-editor/src/editors/v1/core/Editor.ts b/packages/super-editor/src/editors/v1/core/Editor.ts index c636821d48..ae3ea7d95c 100644 --- a/packages/super-editor/src/editors/v1/core/Editor.ts +++ b/packages/super-editor/src/editors/v1/core/Editor.ts @@ -3808,9 +3808,7 @@ export class Editor extends EventEmitter { getUpdatedDocs = false, fieldsHighlightColor = null, compression, - }: ExportDocxParams = {}): Promise< - Blob | Buffer | Record | string | ConvertedXmlPart | undefined - > { + }: ExportDocxParams = {}): Promise | string | ConvertedXmlPart> { try { const exportHostEditor = resolveMainBodyEditor(this); commitLiveStorySessionRuntimes(exportHostEditor); @@ -4081,6 +4079,7 @@ export class Editor extends EventEmitter { const err = error instanceof Error ? error : new Error(String(error)); this.emit('exception', { error: err, editor: this }); console.error(err); + throw err; } } diff --git a/packages/super-editor/src/editors/v1/tests/export/exportDocx.errors.test.js b/packages/super-editor/src/editors/v1/tests/export/exportDocx.errors.test.js new file mode 100644 index 0000000000..dce67d8b5c --- /dev/null +++ b/packages/super-editor/src/editors/v1/tests/export/exportDocx.errors.test.js @@ -0,0 +1,36 @@ +import { describe, expect, it, vi } from 'vitest'; +import { Editor } from '@core/Editor.js'; + +const SAMPLE_JSON = { + type: 'doc', + attrs: { attrs: null }, + content: [ + { + type: 'paragraph', + content: [{ type: 'text', text: 'Export errors should reach the caller' }], + }, + ], +}; + +describe('Editor.exportDocx() error handling', () => { + it('emits an exception event and rejects when export fails', async () => { + const editor = await Editor.open(undefined, { json: SAMPLE_JSON }); + const exportError = new Error('export failed'); + const exceptionListener = vi.fn(); + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + editor.on('exception', exceptionListener); + vi.spyOn(editor.converter, 'exportToDocx').mockRejectedValue(exportError); + + try { + await expect(editor.exportDocx({ exportXmlOnly: true })).rejects.toBe(exportError); + + expect(exceptionListener).toHaveBeenCalledTimes(1); + expect(exceptionListener).toHaveBeenCalledWith({ error: exportError, editor }); + expect(consoleErrorSpy).toHaveBeenCalledWith(exportError); + } finally { + consoleErrorSpy.mockRestore(); + editor.destroy(); + } + }); +}); From 177bd3ec285defaf4a0d0e0b5d474891fd024ad2 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Sat, 6 Jun 2026 10:52:08 -0300 Subject: [PATCH 2/2] fix: de-dupe SuperDoc export exceptions --- packages/superdoc/src/core/SuperDoc.test.js | 85 +++++++++++++++++++++ packages/superdoc/src/core/SuperDoc.ts | 74 ++++++++++-------- 2 files changed, 129 insertions(+), 30 deletions(-) diff --git a/packages/superdoc/src/core/SuperDoc.test.js b/packages/superdoc/src/core/SuperDoc.test.js index 874aad903f..3b0d8a2fd9 100644 --- a/packages/superdoc/src/core/SuperDoc.test.js +++ b/packages/superdoc/src/core/SuperDoc.test.js @@ -1218,6 +1218,91 @@ describe('SuperDoc core', () => { expect(results).toEqual([originalBlob]); }); + it('falls back to original document data and keeps sibling exports when an editor export rejects', async () => { + createAppHarness(); + const onException = vi.fn(); + + const instance = new SuperDoc({ + selector: '#host', + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {}, toolbar: {} }, + colors: [], + user: { name: 'Jane', email: 'jane@example.com' }, + onException, + }); + await flushMicrotasks(); + + const exportError = new Error('export failed'); + const originalBlob = new Blob(['fallback'], { type: DOCX }); + const siblingBlob = new Blob(['exported'], { type: DOCX }); + const failedExportDocxMock = vi.fn().mockRejectedValue(exportError); + const siblingExportDocxMock = vi.fn().mockResolvedValue(siblingBlob); + const failedDoc = { + id: 'doc-1', + type: DOCX, + data: originalBlob, + getEditor: () => ({ exportDocx: failedExportDocxMock }), + }; + const siblingDoc = { + id: 'doc-2', + type: DOCX, + data: null, + getEditor: () => ({ exportDocx: siblingExportDocxMock }), + }; + + instance.superdocStore.documents = [failedDoc, siblingDoc]; + + const results = await instance.exportEditorsToDOCX(); + + expect(failedExportDocxMock).toHaveBeenCalledTimes(1); + expect(siblingExportDocxMock).toHaveBeenCalledTimes(1); + expect(results).toEqual([originalBlob, siblingBlob]); + expect(onException).toHaveBeenCalledTimes(1); + expect(onException).toHaveBeenCalledWith({ error: exportError, document: failedDoc }); + }); + + it('does not emit a duplicate wrapper exception when the editor already bridged the export error', async () => { + createAppHarness(); + const onException = vi.fn(); + + const instance = new SuperDoc({ + selector: '#host', + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {}, toolbar: {} }, + colors: [], + user: { name: 'Jane', email: 'jane@example.com' }, + onException, + }); + await flushMicrotasks(); + + const exportError = new Error('export failed'); + const originalBlob = new Blob(['fallback'], { type: DOCX }); + const editor = { + exportDocx: vi.fn(async () => { + instance.emit('exception', { error: exportError, editor }); + throw exportError; + }), + }; + + instance.superdocStore.documents = [ + { + id: 'doc-1', + type: DOCX, + data: originalBlob, + getEditor: () => editor, + }, + ]; + + const results = await instance.exportEditorsToDOCX(); + + expect(editor.exportDocx).toHaveBeenCalledTimes(1); + expect(results).toEqual([originalBlob]); + expect(onException).toHaveBeenCalledTimes(1); + expect(onException).toHaveBeenCalledWith({ error: exportError, editor }); + }); + it('drops non-DOCX fallback data when an editor export yields no blob', async () => { const { superdocStore } = createAppHarness(); diff --git a/packages/superdoc/src/core/SuperDoc.ts b/packages/superdoc/src/core/SuperDoc.ts index d4c8579b77..67be3eddbc 100644 --- a/packages/superdoc/src/core/SuperDoc.ts +++ b/packages/superdoc/src/core/SuperDoc.ts @@ -2623,39 +2623,53 @@ export class SuperDoc extends EventEmitter { // else: UI store unhydrated → leave `comments` undefined and // let the engine's `converter.comments` fallback fire. - const docxPromises = this.#requireSuperdocStore('exportEditorsToDOCX').documents.map( - async (doc: RuntimeDocument) => { - if (!doc || doc.type !== DOCX) return null; - - const editor = typeof doc.getEditor === 'function' ? doc.getEditor() : null; - const fallbackDocx = () => { - if (!doc.data) return null; - if (doc.data.type && doc.data.type !== DOCX) return null; - return doc.data; - }; - - if (!editor) return fallbackDocx(); + const bridgedExportErrors = new WeakSet(); + const rememberBridgedExportError = (payload: SuperDocExceptionPayload) => { + if ('editor' in payload && payload.error && typeof payload.error === 'object') { + bridgedExportErrors.add(payload.error); + } + }; - try { - const exported = await editor.exportDocx({ - isFinalDoc, - comments: comments as import('@superdoc/super-editor').Comment[] | undefined, - commentsType, - fieldsHighlightColor, - }); - if (exported) return exported; - } catch (error) { - this.emit('exception', { error, document: doc }); - } + this.on('exception', rememberBridgedExportError); + try { + const docxPromises = this.#requireSuperdocStore('exportEditorsToDOCX').documents.map( + async (doc: RuntimeDocument) => { + if (!doc || doc.type !== DOCX) return null; + + const editor = typeof doc.getEditor === 'function' ? doc.getEditor() : null; + const fallbackDocx = () => { + if (!doc.data) return null; + if (doc.data.type && doc.data.type !== DOCX) return null; + return doc.data; + }; + + if (!editor) return fallbackDocx(); + + try { + const exported = await editor.exportDocx({ + isFinalDoc, + comments: comments as import('@superdoc/super-editor').Comment[] | undefined, + commentsType, + fieldsHighlightColor, + }); + if (exported) return exported; + } catch (error) { + if (!error || typeof error !== 'object' || !bridgedExportErrors.has(error)) { + this.emit('exception', { error, document: doc }); + } + } - return fallbackDocx(); - }, - ); + return fallbackDocx(); + }, + ); - const docxFiles = await Promise.all(docxPromises); - // Type-predicate filter so callers see `Blob[]` instead of `(Blob | null)[]`. - // `filter(Boolean)` narrows at runtime but not in the type system. - return docxFiles.filter((file): file is Blob => file != null); + const docxFiles = await Promise.all(docxPromises); + // Type-predicate filter so callers see `Blob[]` instead of `(Blob | null)[]`. + // `filter(Boolean)` narrows at runtime but not in the type system. + return docxFiles.filter((file): file is Blob => file != null); + } finally { + this.off('exception', rememberBridgedExportError); + } } /**