From 962eb6206a1f982aa358cfabf04ea9599c0f6b5d Mon Sep 17 00:00:00 2001 From: Ujjwal Sharma Date: Tue, 8 Oct 2024 10:51:20 +0200 Subject: [PATCH 1/3] Break messageBar class out Move .messageBar out of .dialog into its own standalone class in order to reuse as much of it for the upcoming feature for an undo message for annotations. --- web/dialog.css | 93 ++------------------------------ web/message_bar.css | 127 ++++++++++++++++++++++++++++++++++++++++++++ web/pdf_viewer.css | 1 + 3 files changed, 132 insertions(+), 89 deletions(-) create mode 100644 web/message_bar.css diff --git a/web/dialog.css b/web/dialog.css index c36393fc3..3a5ba15d4 100644 --- a/web/dialog.css +++ b/web/dialog.css @@ -270,29 +270,17 @@ } .messageBar { - --message-bar-warning-icon: url(images/messageBar_warning.svg); - --closing-button-icon: url(images/messageBar_closingButton.svg); - --message-bar-bg-color: #ffebcd; --message-bar-fg-color: #15141a; --message-bar-border-color: rgb(0 0 0 / 0.08); + --message-bar-icon: url(images/messageBar_warning.svg); --message-bar-icon-color: #cd411e; - --message-bar-close-button-border-radius: 4px; - --message-bar-close-button-border: none; - --message-bar-close-button-color: var(--text-primary-color); - --message-bar-close-button-hover-bg-color: rgb(21 20 26 / 0.14); - --message-bar-close-button-active-bg-color: rgb(21 20 26 / 0.21); - --message-bar-close-button-focus-bg-color: rgb(21 20 26 / 0.07); - --message-bar-close-button-color-hover: var(--text-primary-color); @media (prefers-color-scheme: dark) { --message-bar-bg-color: #5a3100; --message-bar-fg-color: #fbfbfe; --message-bar-border-color: rgb(255 255 255 / 0.08); --message-bar-icon-color: #e49c49; - --message-bar-close-button-hover-bg-color: rgb(251 251 254 / 0.14); - --message-bar-close-button-active-bg-color: rgb(251 251 254 / 0.21); - --message-bar-close-button-focus-bg-color: rgb(251 251 254 / 0.07); } @media screen and (forced-colors: active) { @@ -300,43 +288,14 @@ --message-bar-fg-color: CanvasText; --message-bar-border-color: CanvasText; --message-bar-icon-color: CanvasText; - --message-bar-close-button-color: ButtonText; - --message-bar-close-button-border: 1px solid ButtonText; - --message-bar-close-button-hover-bg-color: ButtonText; - --message-bar-close-button-active-bg-color: ButtonText; - --message-bar-close-button-focus-bg-color: ButtonText; - --message-bar-close-button-color-hover: HighlightText; } - display: flex; - position: relative; - padding: 12px 8px 12px 0; - flex-direction: column; - justify-content: center; - align-items: flex-start; - gap: 8px; align-self: stretch; - border-radius: 4px; - border: 1px solid var(--message-bar-border-color); - background: var(--message-bar-bg-color); - color: var(--message-bar-fg-color); - > div { - display: flex; - padding-inline-start: 16px; - align-items: flex-start; - gap: 8px; - align-self: stretch; - - &::before { - content: ""; - display: inline-block; - width: 16px; - height: 16px; - mask-image: var(--message-bar-warning-icon); - mask-size: cover; - background-color: var(--message-bar-icon-color); + &::before, + > div { + margin-block: 4px; } > div { @@ -356,50 +315,6 @@ } } } - - .closeButton { - position: absolute; - width: 32px; - height: 32px; - inset-inline-end: 8px; - inset-block-start: 8px; - background: none; - border-radius: var(--message-bar-close-button-border-radius); - border: var(--message-bar-close-button-border); - - &::before { - content: ""; - display: inline-block; - width: 16px; - height: 16px; - mask-image: var(--closing-button-icon); - mask-size: cover; - background-color: var(--message-bar-close-button-color); - } - - &:is(:hover, :active, :focus)::before { - background-color: var(--message-bar-close-button-color-hover); - } - - &:hover { - background-color: var(--message-bar-close-button-hover-bg-color); - } - - &:active { - background-color: var(--message-bar-close-button-active-bg-color); - } - - &:focus { - background-color: var(--message-bar-close-button-focus-bg-color); - } - - > span { - display: inline-block; - width: 0; - height: 0; - overflow: hidden; - } - } } .toggler { diff --git a/web/message_bar.css b/web/message_bar.css new file mode 100644 index 000000000..2e908f478 --- /dev/null +++ b/web/message_bar.css @@ -0,0 +1,127 @@ +/* Copyright 2024 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.messageBar { + --closing-button-icon: url(images/messageBar_closingButton.svg); + --message-bar-close-button-color: var(--text-primary-color); + --message-bar-close-button-color-hover: var(--text-primary-color); + --message-bar-close-button-border-radius: 4px; + --message-bar-close-button-border: none; + --message-bar-close-button-hover-bg-color: rgb(21 20 26 / 0.14); + --message-bar-close-button-active-bg-color: rgb(21 20 26 / 0.21); + --message-bar-close-button-focus-bg-color: rgb(21 20 26 / 0.07); + + @media (prefers-color-scheme: dark) { + --message-bar-close-button-hover-bg-color: rgb(251 251 254 / 0.14); + --message-bar-close-button-active-bg-color: rgb(251 251 254 / 0.21); + --message-bar-close-button-focus-bg-color: rgb(251 251 254 / 0.07); + } + + @media screen and (forced-colors: active) { + --message-bar-close-button-color: ButtonText; + --message-bar-close-button-border: 1px solid ButtonText; + --message-bar-close-button-hover-bg-color: ButtonText; + --message-bar-close-button-active-bg-color: ButtonText; + --message-bar-close-button-focus-bg-color: ButtonText; + --message-bar-close-button-color-hover: HighlightText; + } + + display: flex; + position: relative; + padding: 8px 8px 8px 16px; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + user-select: none; + + border-radius: 4px; + + border: 1px solid var(--message-bar-border-color); + background: var(--message-bar-bg-color); + color: var(--message-bar-fg-color); + + > div { + display: flex; + align-items: flex-start; + gap: 8px; + align-self: stretch; + + &::before { + content: ""; + display: inline-block; + width: 16px; + height: 16px; + mask-image: var(--message-bar-icon); + mask-size: cover; + background-color: var(--message-bar-icon-color); + flex-shrink: 0; + } + } + + button { + cursor: pointer; + + &:focus-visible { + outline: var(--focus-ring-outline); + outline-offset: 2px; + } + } + + .closeButton { + width: 32px; + height: 32px; + background: none; + border-radius: var(--message-bar-close-button-border-radius); + border: var(--message-bar-close-button-border); + + display: flex; + align-items: center; + justify-content: center; + + &::before { + content: ""; + display: inline-block; + width: 16px; + height: 16px; + mask-image: var(--closing-button-icon); + mask-size: cover; + background-color: var(--message-bar-close-button-color); + } + + &:is(:hover, :active, :focus)::before { + background-color: var(--message-bar-close-button-color-hover); + } + + &:hover { + background-color: var(--message-bar-close-button-hover-bg-color); + } + + &:active { + background-color: var(--message-bar-close-button-active-bg-color); + } + + &:focus { + background-color: var(--message-bar-close-button-focus-bg-color); + } + + > span { + display: inline-block; + width: 0; + height: 0; + overflow: hidden; + } + } +} diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css index ec7bceb69..115a8f853 100644 --- a/web/pdf_viewer.css +++ b/web/pdf_viewer.css @@ -12,6 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import url(message_bar.css); @import url(dialog.css); @import url(text_layer_builder.css); @import url(annotation_layer_builder.css); From dd82d78a2dd82f7474136ff7efb002331e0ba586 Mon Sep 17 00:00:00 2001 From: Ujjwal Sharma Date: Tue, 22 Oct 2024 13:23:47 +0530 Subject: [PATCH 2/3] Pop open a message when user deletes an annotation When a user deletes any number of annotations, they are notified of the action by a popup message with an undo button. Besides that, this change reuses the existing messageBar CSS class from the new alt-text dialog as much as possible. --- gulpfile.mjs | 1 + l10n/en-US/viewer.ftl | 21 ++++++ src/display/editor/draw.js | 1 + src/display/editor/editor.js | 2 + src/display/editor/tools.js | 14 +++- web/app.js | 37 +++++++++- web/editor_undo_bar.js | 128 +++++++++++++++++++++++++++++++++++ web/message_bar.css | 94 +++++++++++++++++++++++++ web/pdf_viewer.js | 6 +- web/viewer.html | 14 ++++ web/viewer.js | 6 ++ 11 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 web/editor_undo_bar.js diff --git a/gulpfile.mjs b/gulpfile.mjs index 539e199cf..57c8c66d1 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -1148,6 +1148,7 @@ function buildComponents(defines, dir) { "web/images/messageBar_*.svg", "web/images/toolbarButton-{editorHighlight,menuArrow}.svg", "web/images/cursor-*.svg", + "web/images/secondaryToolbarButton-documentProperties.svg", ]; return ordered([ diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index fbff0d86a..3e4a3510c 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -503,3 +503,24 @@ pdfjs-editor-alt-text-settings-editor-title = Alt text editor pdfjs-editor-alt-text-settings-show-dialog-button-label = Show alt text editor right away when adding an image pdfjs-editor-alt-text-settings-show-dialog-description = Helps you make sure all your images have alt text. pdfjs-editor-alt-text-settings-close-button = Close + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Highlight removed +pdfjs-editor-undo-bar-message-freetext = Text removed +pdfjs-editor-undo-bar-message-ink = Drawing removed +pdfjs-editor-undo-bar-message-stamp = Image removed +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } annotation removed + *[other] { $count } annotations removed + } + +pdfjs-editor-undo-bar-undo-button = + .title = Undo +pdfjs-editor-undo-bar-undo-button-label = Undo +pdfjs-editor-undo-bar-close-button = + .title = Close +pdfjs-editor-undo-bar-close-button-label = Close diff --git a/src/display/editor/draw.js b/src/display/editor/draw.js index 3906fc4ee..d5f1c4bb0 100644 --- a/src/display/editor/draw.js +++ b/src/display/editor/draw.js @@ -676,6 +676,7 @@ class DrawingEditor extends AnnotationEditor { signal, }); parent.toggleDrawing(); + uiManager._editorUndoBar?.hide(); if (this._currentDraw) { parent.drawLayer.updateProperties( diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 026d22574..50aff8624 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -1142,6 +1142,8 @@ class AnnotationEditor { bindEvents(this, this.div, ["pointerdown"]); + this._uiManager._editorUndoBar?.hide(); + return this.div; } diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index d60832488..cb1d9e24b 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -620,6 +620,8 @@ class AnnotationEditorUIManager { #editorsToRescale = new Set(); + _editorUndoBar = null; + #enableHighlightFloatingButton = false; #enableUpdatedAddImage = false; @@ -829,7 +831,8 @@ class AnnotationEditorUIManager { enableHighlightFloatingButton, enableUpdatedAddImage, enableNewAltTextWhenAddingImage, - mlManager + mlManager, + editorUndoBar ) { const signal = (this._signal = this.#abortController.signal); this.#container = container; @@ -864,6 +867,7 @@ class AnnotationEditorUIManager { rotation: 0, }; this.isShiftKeyDown = false; + this._editorUndoBar = editorUndoBar || null; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { Object.defineProperty(this, "reset", { @@ -904,6 +908,7 @@ class AnnotationEditorUIManager { clearTimeout(this.#translationTimeoutId); this.#translationTimeoutId = null; } + this._editorUndoBar?.destroy(); } combinedSignal(ac) { @@ -1656,6 +1661,8 @@ class AnnotationEditorUIManager { this.setEditingState(false); this.#disableAll(); + this._editorUndoBar?.hide(); + this.#updateModeCapability.resolve(); return; } @@ -2038,6 +2045,7 @@ class AnnotationEditorUIManager { hasSomethingToRedo: true, isEmpty: this.#isEmpty(), }); + this._editorUndoBar?.hide(); } /** @@ -2099,6 +2107,10 @@ class AnnotationEditorUIManager { ? [drawingEditor] : [...this.#selectedEditors]; const cmd = () => { + this._editorUndoBar?.show( + undo, + editors.length === 1 ? editors[0].editorType : editors.length + ); for (const editor of editors) { editor.remove(); } diff --git a/web/app.js b/web/app.js index aa4bba755..e45c954b9 100644 --- a/web/app.js +++ b/web/app.js @@ -69,6 +69,7 @@ import { AltTextManager } from "web-alt_text_manager"; import { AnnotationEditorParams } from "web-annotation_editor_params"; import { CaretBrowsingMode } from "./caret_browsing.js"; import { DownloadManager } from "web-download_manager"; +import { EditorUndoBar } from "./editor_undo_bar.js"; import { OverlayManager } from "./overlay_manager.js"; import { PasswordPrompt } from "./password_prompt.js"; import { PDFAttachmentViewer } from "web-pdf_attachment_viewer"; @@ -192,6 +193,7 @@ const PDFViewerApplication = { _isCtrlKeyDown: false, _caretBrowsing: null, _isScrolling: false, + editorUndoBar: null, // Called once when the document is loaded. async initialize(appConfig) { @@ -461,6 +463,10 @@ const PDFViewerApplication = { : null; } + if (appConfig.editorUndoBar) { + this.editorUndoBar = new EditorUndoBar(appConfig.editorUndoBar, eventBus); + } + const enableHWA = AppOptions.get("enableHWA"); const pdfViewer = new PDFViewer({ container, @@ -470,6 +476,7 @@ const PDFViewerApplication = { linkService: pdfLinkService, downloadManager, altTextManager, + editorUndoBar: this.editorUndoBar, findController, scriptingManager: AppOptions.get("enableScripting") && pdfScriptingManager, @@ -2732,7 +2739,7 @@ function onTouchEnd(evt) { this._isPinching = false; } -function onClick(evt) { +function closeSecondaryToolbar(evt) { if (!this.secondaryToolbar?.isOpen) { return; } @@ -2749,6 +2756,20 @@ function onClick(evt) { } } +function closeEditorUndoBar(evt) { + if (!this.editorUndoBar?.isOpen) { + return; + } + if (this.appConfig.secondaryToolbar?.toolbar.contains(evt.target)) { + this.editorUndoBar.hide(); + } +} + +function onClick(evt) { + closeSecondaryToolbar.call(this, evt); + closeEditorUndoBar.call(this, evt); +} + function onKeyUp(evt) { // evt.ctrlKey is false hence we use evt.key. if (evt.key === "Control") { @@ -2759,6 +2780,20 @@ function onKeyUp(evt) { function onKeyDown(evt) { this._isCtrlKeyDown = evt.key === "Control"; + if ( + this.editorUndoBar?.isOpen && + evt.keyCode !== 9 && + evt.keyCode !== 16 && + !( + (evt.keyCode === 13 || evt.keyCode === 32) && + getActiveOrFocusedElement() === this.appConfig.editorUndoBar.undoButton + ) + ) { + // Hide undo bar on keypress except for Shift, Tab, Shift+Tab. + // Also avoid hiding if the undo button is triggered. + this.editorUndoBar.hide(); + } + if (this.overlayManager.active) { return; } diff --git a/web/editor_undo_bar.js b/web/editor_undo_bar.js new file mode 100644 index 000000000..aa064e384 --- /dev/null +++ b/web/editor_undo_bar.js @@ -0,0 +1,128 @@ +/* Copyright 2024 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { noContextMenu } from "pdfjs-lib"; + +class EditorUndoBar { + #closeButton = null; + + #container; + + #eventBus = null; + + #focusTimeout = null; + + #initController = null; + + isOpen = false; + + #message; + + #showController = null; + + #undoButton; + + static #l10nMessages = Object.freeze({ + highlight: "pdfjs-editor-undo-bar-message-highlight", + freetext: "pdfjs-editor-undo-bar-message-freetext", + stamp: "pdfjs-editor-undo-bar-message-stamp", + ink: "pdfjs-editor-undo-bar-message-ink", + _multiple: "pdfjs-editor-undo-bar-message-multiple", + }); + + constructor({ container, message, undoButton, closeButton }, eventBus) { + this.#container = container; + this.#message = message; + this.#undoButton = undoButton; + this.#closeButton = closeButton; + this.#eventBus = eventBus; + } + + destroy() { + this.#initController?.abort(); + this.#initController = null; + + this.hide(); + } + + show(undoAction, messageData) { + if (!this.#initController) { + this.#initController = new AbortController(); + const opts = { signal: this.#initController.signal }; + const boundHide = this.hide.bind(this); + + this.#container.addEventListener("contextmenu", noContextMenu, opts); + this.#closeButton.addEventListener("click", boundHide, opts); + this.#eventBus._on("beforeprint", boundHide, opts); + this.#eventBus._on("download", boundHide, opts); + } + + this.hide(); + + if (typeof messageData === "string") { + this.#message.setAttribute( + "data-l10n-id", + EditorUndoBar.#l10nMessages[messageData] + ); + } else { + this.#message.setAttribute( + "data-l10n-id", + EditorUndoBar.#l10nMessages._multiple + ); + this.#message.setAttribute( + "data-l10n-args", + JSON.stringify({ count: messageData }) + ); + } + this.isOpen = true; + this.#container.hidden = false; + + this.#showController = new AbortController(); + + this.#undoButton.addEventListener( + "click", + () => { + undoAction(); + this.hide(); + }, + { signal: this.#showController.signal } + ); + + // Without the setTimeout, VoiceOver will read out the document title + // instead of the popup label. + this.#focusTimeout = setTimeout(() => { + this.#container.focus(); + this.#focusTimeout = null; + }, 100); + } + + hide() { + if (!this.isOpen) { + return; + } + this.isOpen = false; + this.#container.hidden = true; + + this.#showController?.abort(); + this.#showController = null; + + if (this.#focusTimeout) { + clearTimeout(this.#focusTimeout); + this.#focusTimeout = null; + } + } +} + +export { EditorUndoBar }; diff --git a/web/message_bar.css b/web/message_bar.css index 2e908f478..4a948d2ed 100644 --- a/web/message_bar.css +++ b/web/message_bar.css @@ -125,3 +125,97 @@ } } } + +#editorUndoBar { + --text-primary-color: #15141a; + + --message-bar-icon: url(images/secondaryToolbarButton-documentProperties.svg); + --message-bar-icon-color: #0060df; + --message-bar-bg-color: #deeafc; + --message-bar-fg-color: var(--text-primary-color); + --message-bar-border-color: rgb(0 0 0 / 0.08); + + --undo-button-bg-color: rgb(21 20 26 / 0.07); + --undo-button-bg-color-hover: rgb(21 20 26 / 0.14); + --undo-button-bg-color-active: rgb(21 20 26 / 0.21); + + --undo-button-fg-color: var(--message-bar-fg-color); + --undo-button-fg-color-hover: var(--undo-button-fg-color); + --undo-button-fg-color-active: var(--undo-button-fg-color); + + --focus-ring-color: #0060df; + --focus-ring-outline: 2px solid var(--focus-ring-color); + + @media (prefers-color-scheme: dark) { + --text-primary-color: #fbfbfe; + + --message-bar-icon-color: #73a7f3; + --message-bar-bg-color: #003070; + --message-bar-border-color: rgb(255 255 255 / 0.08); + + --undo-button-bg-color: rgb(255 255 255 / 0.08); + --undo-button-bg-color-hover: rgb(255 255 255 / 0.14); + --undo-button-bg-color-active: rgb(255 255 255 / 0.21); + } + + @media screen and (forced-colors: active) { + --text-primary-color: CanvasText; + + --message-bar-icon-color: CanvasText; + --message-bar-bg-color: Canvas; + --message-bar-border-color: CanvasText; + + --undo-button-bg-color: ButtonText; + --undo-button-bg-color-hover: SelectedItem; + --undo-button-bg-color-active: SelectedItem; + + --undo-button-fg-color: ButtonFace; + --undo-button-fg-color-hover: SelectedItemText; + --undo-button-fg-color-active: SelectedItemText; + + --focus-ring-color: CanvasText; + } + + position: fixed; + top: 50px; + left: 50%; + transform: translateX(-50%); + z-index: 10; + + padding-block: 8px; + padding-inline: 16px 8px; + + font: menu; + font-size: 15px; + + cursor: default; + + button { + cursor: pointer; + } + + #editorUndoBarUndoButton { + border-radius: 4px; + font-weight: 590; + line-height: 19.5px; + color: var(--undo-button-fg-color); + border: none; + padding: 4px 16px; + margin-inline-start: 8px; + height: 32px; + + background-color: var(--undo-button-bg-color); + + &:hover { + background-color: var(--undo-button-bg-color-hover); + } + + &:active { + background-color: var(--undo-button-bg-color-active); + } + } + + > div { + align-items: center; + } +} diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 6f05d56fe..e968db3f1 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -214,6 +214,8 @@ class PDFViewer { #containerTopLeft = null; + #editorUndoBar = null; + #enableHWA = false; #enableHighlightFloatingButton = false; @@ -281,6 +283,7 @@ class PDFViewer { this.downloadManager = options.downloadManager || null; this.findController = options.findController || null; this.#altTextManager = options.altTextManager || null; + this.#editorUndoBar = options.editorUndoBar || null; if (this.findController) { this.findController.onIsPageVisible = pageNumber => @@ -907,7 +910,8 @@ class PDFViewer { this.#enableHighlightFloatingButton, this.#enableUpdatedAddImage, this.#enableNewAltTextWhenAddingImage, - this.#mlManager + this.#mlManager, + this.#editorUndoBar ); eventBus.dispatch("annotationeditoruimanager", { source: this, diff --git a/web/viewer.html b/web/viewer.html index dc8020d32..bee574ad9 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -688,6 +688,20 @@ See https://github.com/adobe-type-tools/cmap-resources + +
diff --git a/web/viewer.js b/web/viewer.js index 30eefd022..4cab20308 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -223,6 +223,12 @@ function getViewerConfiguration() { editorHighlightShowAll: document.getElementById("editorHighlightShowAll"), }, printContainer: document.getElementById("printContainer"), + editorUndoBar: { + container: document.getElementById("editorUndoBar"), + message: document.getElementById("editorUndoBarMessage"), + undoButton: document.getElementById("editorUndoBarUndoButton"), + closeButton: document.getElementById("editorUndoBarCloseButton"), + }, }; } From 41bf874461370e9d1c4f9d8ffb9da13bda09db56 Mon Sep 17 00:00:00 2001 From: Aditi <62544124+Aditi-1400@users.noreply.github.com> Date: Sat, 19 Oct 2024 02:19:46 +0530 Subject: [PATCH 3/3] Add tests for annotation delete popup --- test/integration/freetext_editor_spec.mjs | 121 ++++++ test/integration/highlight_editor_spec.mjs | 448 +++++++++++++++++++++ test/integration/ink_editor_spec.mjs | 126 ++++++ test/integration/stamp_editor_spec.mjs | 94 +++++ test/integration/test_utils.mjs | 7 + 5 files changed, 796 insertions(+) diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index 0c3f9d724..085c75e6a 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -3670,4 +3670,125 @@ describe("FreeText Editor", () => { ); }); }); + + describe("Undo deletion popup has the expected behaviour", () => { + let pages; + const editorSelector = getEditorSelector(0); + + beforeEach(async () => { + pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that deleting a FreeText editor can be undone using the undo button", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToFreeText(page); + + const rect = await getRect(page, ".annotationEditorLayer"); + const data = "Hello PDF.js World !!"; + await page.mouse.click(rect.x + 100, rect.y + 100); + await page.waitForSelector(editorSelector, { + visible: true, + }); + await page.type(`${editorSelector} .internal`, data); + + // Commit. + await page.keyboard.press("Escape"); + await page.waitForSelector(`${editorSelector} .overlay.enabled`); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForSelector("#editorUndoBar:not([hidden])"); + await page.click("#editorUndoBarUndoButton"); + await waitForSerialized(page, 1); + await page.waitForSelector(editorSelector); + }) + ); + }); + + it("must check that the undo deletion popup displays the correct message", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToFreeText(page); + + const rect = await getRect(page, ".annotationEditorLayer"); + const data = "Hello PDF.js World !!"; + await page.mouse.click(rect.x + 100, rect.y + 100); + await page.waitForSelector(editorSelector, { + visible: true, + }); + await page.type(`${editorSelector} .internal`, data); + + // Commit. + await page.keyboard.press("Escape"); + await page.waitForSelector(`${editorSelector} .overlay.enabled`); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForFunction(() => { + const messageElement = document.querySelector( + "#editorUndoBarMessage" + ); + return messageElement && messageElement.textContent.trim() !== ""; + }); + const message = await page.waitForSelector("#editorUndoBarMessage"); + const messageText = await page.evaluate( + el => el.textContent, + message + ); + expect(messageText).toContain("Text removed"); + }) + ); + }); + + it("must check that the popup disappears when a new textbox is created", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToFreeText(page); + + let rect = await getRect(page, ".annotationEditorLayer"); + const data = "Hello PDF.js World !!"; + await page.mouse.click(rect.x + 100, rect.y + 100); + await page.waitForSelector(editorSelector, { + visible: true, + }); + await page.type(`${editorSelector} .internal`, data); + + await page.keyboard.press("Escape"); + await page.waitForSelector(`${editorSelector} .overlay.enabled`); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForSelector("#editorUndoBar:not([hidden])"); + rect = await getRect(page, ".annotationEditorLayer"); + const newData = "This is a new text box!"; + await page.mouse.click(rect.x + 150, rect.y + 150); + await page.waitForSelector(getEditorSelector(1), { + visible: true, + }); + await page.type(`${getEditorSelector(1)} .internal`, newData); + + await page.keyboard.press("Escape"); + await page.waitForSelector( + `${getEditorSelector(1)} .overlay.enabled` + ); + await waitForSerialized(page, 1); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + }); }); diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index c6cbd1f8a..19bcf30ca 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -26,6 +26,7 @@ import { kbBigMoveUp, kbFocusNext, kbFocusPrevious, + kbSave, kbSelectAll, kbUndo, loadAndWait, @@ -37,6 +38,11 @@ import { waitForSelectedEditor, waitForSerialized, } from "./test_utils.mjs"; +import { fileURLToPath } from "url"; +import fs from "fs"; +import path from "path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const selectAll = async page => { await kbSelectAll(page); @@ -2165,4 +2171,446 @@ describe("Highlight Editor", () => { ); }); }); + + describe("Undo deletion popup has the expected behaviour", () => { + let pages; + const editorSelector = getEditorSelector(0); + + beforeEach(async () => { + pages = await loadAndWait( + "tracemonkey.pdf", + ".annotationEditorLayer", + null, + null, + { + highlightEditorColors: + "yellow=#FFFF00,green=#00FF00,blue=#0000FF,pink=#FF00FF,red=#FF0000", + } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that deleting a highlight can be undone using the undo button", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.click("#editorUndoBarUndoButton"); + await waitForSerialized(page, 1); + await page.waitForSelector(editorSelector); + await page.waitForSelector( + `.page[data-page-number = "1"] svg.highlight[fill = "#FFFF00"]` + ); + }) + ); + }); + + it("must check that the popup disappears when the undo button is clicked", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.click("#editorUndoBarUndoButton"); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must check that the popup disappears when the close button is clicked", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.waitForSelector("#editorUndoBarCloseButton"); + await page.click("#editorUndoBarCloseButton"); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must check that the popup disappears when a new annotation is created", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + const newRect = await getSpanRectFromText(page, 1, "Introduction"); + const newX = newRect.x + newRect.width / 2; + const newY = newRect.y + newRect.height / 2; + await page.mouse.click(newX, newY, { count: 2, delay: 100 }); + + await page.waitForSelector(getEditorSelector(1)); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must check that the popup disappears when the print dialog is opened", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.evaluate(() => window.print()); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must check that the popup disappears when the user clicks on the print button", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.click("#printButton"); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must check that the popup disappears when the save dialog is opened", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await kbSave(page); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must check that the popup disappears when an option from the secondaryToolbar is used", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.click("#secondaryToolbarToggleButton"); + await page.click("#lastPage"); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must check that the popup disappears when highlight mode is disabled", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await switchToHighlight(page, /* disable */ true); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must check that the popup disappears when a PDF is drag-and-dropped", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + const pdfPath = path.join(__dirname, "../pdfs/basicapi.pdf"); + const pdfData = fs.readFileSync(pdfPath).toString("base64"); + const dataTransfer = await page.evaluateHandle(data => { + const transfer = new DataTransfer(); + const view = Uint8Array.from(atob(data), code => + code.charCodeAt(0) + ); + const file = new File([view], "basicapi.pdf", { + type: "application/pdf", + }); + transfer.items.add(file); + return transfer; + }, pdfData); + + const dropSelector = "#viewer"; + await page.evaluate( + (transfer, selector) => { + const dropTarget = document.querySelector(selector); + const event = new DragEvent("dragstart", { + dataTransfer: transfer, + }); + dropTarget.dispatchEvent(event); + }, + dataTransfer, + dropSelector + ); + + await page.evaluate( + (transfer, selector) => { + const dropTarget = document.querySelector(selector); + const event = new DragEvent("drop", { + dataTransfer: transfer, + bubbles: true, + }); + dropTarget.dispatchEvent(event); + }, + dataTransfer, + dropSelector + ); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must check that the undo deletion popup displays the correct message", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForFunction(() => { + const messageElement = document.querySelector( + "#editorUndoBarMessage" + ); + return messageElement && messageElement.textContent.trim() !== ""; + }); + + const message = await page.waitForSelector("#editorUndoBarMessage"); + const messageText = await page.evaluate( + el => el.textContent, + message + ); + expect(messageText).toContain("Highlight removed"); + }) + ); + }); + + it("must display correct message for multiple highlights", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + let rect = await getSpanRectFromText(page, 1, "Abstract"); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + + rect = await getSpanRectFromText(page, 1, "Languages"); + x = rect.x + rect.width / 2; + y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(getEditorSelector(1)); + + await selectAll(page); + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForFunction(() => { + const messageElement = document.querySelector( + "#editorUndoBarMessage" + ); + return messageElement && messageElement.textContent.trim() !== ""; + }); + + const message = await page.waitForSelector("#editorUndoBarMessage"); + const messageText = await page.evaluate( + el => el.textContent, + message + ); + + // Cleans the message text by removing all non-ASCII characters. + // It eliminates any invisible characters such as directional marks + // that interfere with string comparisons + const cleanMessage = messageText.replaceAll(/\P{ASCII}/gu, ""); + expect(cleanMessage).toContain(`2 annotations removed`); + }) + ); + }); + + it("must work properly when selecting undo by keyboard", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.focus("#editorUndoBarUndoButton"); // we have to simulate focus like this to avoid the wait + await page.keyboard.press("Enter"); + await waitForSerialized(page, 1); + await page.waitForSelector(editorSelector); + await page.waitForSelector( + `.page[data-page-number = "1"] svg.highlight[fill = "#FFFF00"]` + ); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.focus("#editorUndoBarUndoButton"); // we have to simulate focus like this to avoid the wait + await page.keyboard.press(" "); + await waitForSerialized(page, 1); + await page.waitForSelector(editorSelector); + await page.waitForSelector( + `.page[data-page-number = "1"] svg.highlight[fill = "#FFFF00"]` + ); + }) + ); + }); + + it("must dismiss itself when user presses space/enter key and undo key isn't focused", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.focus("#editorUndoBar"); + await page.keyboard.press("Enter"); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + }); }); diff --git a/test/integration/ink_editor_spec.mjs b/test/integration/ink_editor_spec.mjs index 1f908fde7..8754ccfd2 100644 --- a/test/integration/ink_editor_spec.mjs +++ b/test/integration/ink_editor_spec.mjs @@ -829,4 +829,130 @@ describe("Ink Editor", () => { ); }); }); + + describe("Undo deletion popup has the expected behaviour", () => { + let pages; + const editorSelector = getEditorSelector(0); + + beforeEach(async () => { + pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that deleting a drawing can be undone using the undo button", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToInk(page); + + const rect = await getRect(page, ".annotationEditorLayer"); + const xStart = rect.x + 300; + const yStart = rect.y + 300; + const clickHandle = await waitForPointerUp(page); + await page.mouse.move(xStart, yStart); + await page.mouse.down(); + await page.mouse.move(xStart + 50, yStart + 50); + await page.mouse.up(); + await awaitPromise(clickHandle); + await commit(page); + + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForSelector("#editorUndoBar:not([hidden])"); + await page.click("#editorUndoBarUndoButton"); + await waitForSerialized(page, 1); + await page.waitForSelector(editorSelector); + }) + ); + }); + + it("must check that the undo deletion popup displays the correct message", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToInk(page); + + const rect = await getRect(page, ".annotationEditorLayer"); + const xStart = rect.x + 300; + const yStart = rect.y + 300; + const clickHandle = await waitForPointerUp(page); + await page.mouse.move(xStart, yStart); + await page.mouse.down(); + await page.mouse.move(xStart + 50, yStart + 50); + await page.mouse.up(); + await awaitPromise(clickHandle); + await commit(page); + + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForFunction(() => { + const messageElement = document.querySelector( + "#editorUndoBarMessage" + ); + return messageElement && messageElement.textContent.trim() !== ""; + }); + const message = await page.waitForSelector("#editorUndoBarMessage"); + const messageText = await page.evaluate( + el => el.textContent, + message + ); + expect(messageText).toContain("Drawing removed"); + }) + ); + }); + + it("must check that the popup disappears when a new drawing is created", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToInk(page); + + const rect = await getRect(page, ".annotationEditorLayer"); + const xStart = rect.x + 300; + const yStart = rect.y + 300; + const clickHandle = await waitForPointerUp(page); + await page.mouse.move(xStart, yStart); + await page.mouse.down(); + await page.mouse.move(xStart + 50, yStart + 50); + await page.mouse.up(); + await awaitPromise(clickHandle); + await commit(page); + + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + const newRect = await getRect(page, ".annotationEditorLayer"); + const newXStart = newRect.x + 300; + const newYStart = newRect.y + 300; + const newClickHandle = await waitForPointerUp(page); + await page.mouse.move(newXStart, newYStart); + await page.mouse.down(); + await page.mouse.move(newXStart + 50, newYStart + 50); + await page.mouse.up(); + await awaitPromise(newClickHandle); + await commit(page); + + await page.waitForSelector(getEditorSelector(1)); + await waitForSerialized(page, 1); + await page.waitForSelector(getEditorSelector(1)); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + }); }); diff --git a/test/integration/stamp_editor_spec.mjs b/test/integration/stamp_editor_spec.mjs index d09d9c3c3..60197b5dd 100644 --- a/test/integration/stamp_editor_spec.mjs +++ b/test/integration/stamp_editor_spec.mjs @@ -1530,4 +1530,98 @@ describe("Stamp Editor", () => { } }); }); + + describe("Undo deletion popup has the expected behaviour", () => { + let pages; + const editorSelector = getEditorSelector(0); + + beforeEach(async () => { + pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that deleting an image can be undone using the undo button", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToStamp(page); + const selector = editorSelector; + + await copyImage(page, "../images/firefox_logo.png", 0); + await page.waitForSelector(selector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${selector} button.delete`); + await page.click(`${selector} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.click("#editorUndoBarUndoButton"); + await waitForSerialized(page, 1); + await page.waitForSelector(editorSelector); + await page.waitForSelector(`${selector} canvas`); + }) + ); + }); + + it("must check that the undo deletion popup displays the correct message", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToStamp(page); + const selector = editorSelector; + + await copyImage(page, "../images/firefox_logo.png", 0); + await page.waitForSelector(selector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${selector} button.delete`); + await page.click(`${selector} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForFunction(() => { + const messageElement = document.querySelector( + "#editorUndoBarMessage" + ); + return messageElement && messageElement.textContent.trim() !== ""; + }); + const message = await page.waitForSelector("#editorUndoBarMessage"); + const messageText = await page.evaluate( + el => el.textContent, + message + ); + expect(messageText).toContain("Image removed"); + }) + ); + }); + + it("must check that the popup disappears when a new image is inserted", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToStamp(page); + const selector = editorSelector; + + await copyImage(page, "../images/firefox_logo.png", 0); + await page.waitForSelector(selector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${editorSelector} button.delete`); + await page.click(`${editorSelector} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForSelector("#editorUndoBar:not([hidden])"); + await page.click("#editorStampAddImage"); + const newInput = await page.$("#stampEditorFileInput"); + await newInput.uploadFile( + `${path.join(__dirname, "../images/firefox_logo.png")}` + ); + await waitForImage(page, getEditorSelector(1)); + await waitForSerialized(page, 1); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + }); }); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index 6138035b0..c52f5257b 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -756,6 +756,12 @@ async function kbFocusPrevious(page) { await awaitPromise(handle); } +async function kbSave(page) { + await page.keyboard.down(modifier); + await page.keyboard.press("s"); + await page.keyboard.up(modifier); +} + async function switchToEditor(name, page, disable = false) { const modeChangedHandle = await createPromise(page, resolve => { window.PDFViewerApplication.eventBus.on( @@ -842,6 +848,7 @@ export { kbModifierDown, kbModifierUp, kbRedo, + kbSave, kbSelectAll, kbUndo, loadAndWait,