diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 45e1441a7066d..11d2a95ab1ea9 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -1109,20 +1109,25 @@ class AnnotationEditor { if (this._editToolbar || this.#isInEditMode) { return this._editToolbar; } - this._editToolbar = new EditorToolbar(this); - this.div.append(this._editToolbar.render()); + const toolbar = (this._editToolbar = new EditorToolbar(this)); + this.div.append(toolbar.render()); const { toolbarButtons } = this; if (toolbarButtons) { for (const [name, tool] of toolbarButtons) { - await this._editToolbar.addButton(name, tool); + await toolbar.addButton(name, tool); + if (this._editToolbar !== toolbar) { + // The editor can be removed while a button is still being added, + // which nulls out `_editToolbar` (see issue 22341). + return this._editToolbar; + } } } if (!this.hasComment) { - this._editToolbar.addButton("comment", this.addCommentButton()); + toolbar.addButton("comment", this.addCommentButton()); } - this._editToolbar.addButton("delete"); + toolbar.addButton("delete"); - return this._editToolbar; + return toolbar; } addCommentButtonInToolbar() { diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index 0eb382d9c0ed3..72f1064156d79 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -3644,4 +3644,91 @@ describe("FreeText Editor", () => { ); }); }); + + describe("Delete a FreeText editor while it is edited (bug 22341)", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("empty.pdf", ".annotationEditorLayer"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must delete the editor without throwing", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToFreeText(page); + + const rect = await getRect(page, ".annotationEditorLayer"); + const editorSelector = getEditorSelector(await getNextEditorId(page)); + await page.mouse.click(rect.x + 100, rect.y + 100); + await page.waitForSelector(`${editorSelector} .internal`, { + visible: true, + }); + await page.type( + `${editorSelector} .internal`, + "Hello PDF.js World !!" + ); + + const isEditing = await page.$eval( + `${editorSelector} .internal`, + el => el.isContentEditable + ); + expect(isEditing).withContext(`In ${browserName}`).toEqual(true); + + const pageErrors = []; + const onPageError = error => { + pageErrors.push(error?.message || String(error)); + }; + page.on("pageerror", onPageError); + + await page.evaluate(() => { + window.pdfjsIntegrationRejections = []; + window.addEventListener("unhandledrejection", event => { + const reason = event.reason; + window.pdfjsIntegrationRejections.push( + reason?.message || String(reason) + ); + }); + }); + + await page.evaluate(() => { + window.PDFViewerApplication.pdfViewer._layerProperties.annotationEditorUIManager.delete(); + }); + + await page.waitForFunction( + sel => !document.querySelector(sel), + {}, + editorSelector + ); + await waitForStorageEntries(page, 0); + + const rejectionErrors = await page.evaluate( + () => + new Promise(resolve => { + setTimeout(() => { + resolve(window.pdfjsIntegrationRejections); + }, 0); + }) + ); + expect(rejectionErrors).withContext(`In ${browserName}`).toEqual([]); + expect(pageErrors).withContext(`In ${browserName}`).toEqual([]); + page.off("pageerror", onPageError); + + // The restored editor must come back with one complete toolbar. + await kbUndo(page); + await page.waitForSelector(editorSelector, { visible: true }); + await selectEditor(page, editorSelector); + await page.waitForSelector(`${editorSelector} button.deleteButton`); + const toolbarCount = await page.evaluate( + sel => document.querySelectorAll(`${sel} .editToolbar`).length, + editorSelector + ); + expect(toolbarCount).withContext(`In ${browserName}`).toEqual(1); + }) + ); + }); + }); });