diff --git a/src/core/annotation.js b/src/core/annotation.js index 195a9cf1ee4a7..6f6c533e13f77 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -1518,7 +1518,8 @@ class WidgetAnnotation extends Annotation { const storageEntry = annotationStorage ? annotationStorage.get(this.data.id) : undefined; - let value = storageEntry && storageEntry.value; + let value = + storageEntry && (storageEntry.formattedValue || storageEntry.value); if (value === undefined) { if (!this._hasValueFromXFA || this.appearance) { // The annotation hasn't been rendered so use the appearance. @@ -1981,7 +1982,7 @@ class TextWidgetAnnotation extends WidgetAnnotation { return { id: this.data.id, value: this.data.fieldValue, - defaultValue: this.data.defaultFieldValue, + defaultValue: this.data.defaultFieldValue || "", multiline: this.data.multiLine, password: this.hasFieldFlag(AnnotationFieldFlag.PASSWORD), charLimit: this.data.maxLen, diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 31edf3580656b..8332a2f796369 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -297,6 +297,110 @@ class AnnotationElement { return container; } + get _commonActions() { + const setColor = (jsName, styleName, event) => { + const color = event.detail[jsName]; + event.target.style[styleName] = ColorConverters[`${color[0]}_HTML`]( + color.slice(1) + ); + }; + + return shadow(this, "_commonActions", { + display: event => { + const hidden = event.detail.display % 2 === 1; + event.target.style.visibility = hidden ? "hidden" : "visible"; + this.annotationStorage.setValue(this.data.id, { + hidden, + print: event.detail.display === 0 || event.detail.display === 3, + }); + }, + print: event => { + this.annotationStorage.setValue(this.data.id, { + print: event.detail.print, + }); + }, + hidden: event => { + event.target.style.visibility = event.detail.hidden + ? "hidden" + : "visible"; + this.annotationStorage.setValue(this.data.id, { + hidden: event.detail.hidden, + }); + }, + focus: event => { + setTimeout(() => event.target.focus({ preventScroll: false }), 0); + }, + userName: event => { + // tooltip + event.target.title = event.detail.userName; + }, + readonly: event => { + if (event.detail.readonly) { + event.target.setAttribute("readonly", ""); + } else { + event.target.removeAttribute("readonly"); + } + }, + required: event => { + if (event.detail.required) { + event.target.setAttribute("required", ""); + } else { + event.target.removeAttribute("required"); + } + }, + bgColor: event => { + setColor("bgColor", "backgroundColor", event); + }, + fillColor: event => { + setColor("fillColor", "backgroundColor", event); + }, + fgColor: event => { + setColor("fgColor", "color", event); + }, + textColor: event => { + setColor("textColor", "color", event); + }, + borderColor: event => { + setColor("borderColor", "borderColor", event); + }, + strokeColor: event => { + setColor("strokeColor", "borderColor", event); + }, + }); + } + + _dispatchEventFromSandbox(actions, jsEvent) { + const commonActions = this._commonActions; + for (const name of Object.keys(jsEvent.detail)) { + const action = actions[name] || commonActions[name]; + if (action) { + action(jsEvent); + } + } + } + + _setDefaultPropertiesFromJS(element) { + if (!this.enableScripting) { + return; + } + + // Some properties may have been updated thanks to JS. + const storedData = this.annotationStorage.getRawValue(this.data.id); + if (!storedData) { + return; + } + + const commonActions = this._commonActions; + for (const [actionName, detail] of Object.entries(storedData)) { + const action = commonActions[actionName]; + if (action) { + action({ detail, target: element }); + // The action has been consumed: no need to keep it. + delete storedData[actionName]; + } + } + } + /** * Create quadrilaterals from the annotation's quadpoints. * @@ -657,7 +761,7 @@ class LinkAnnotationElement extends AnnotationElement { switch (field.type) { case "text": { const value = field.defaultValue || ""; - storage.setValue(id, { value, valueAsString: value }); + storage.setValue(id, { value }); break; } case "checkbox": @@ -794,85 +898,6 @@ class WidgetAnnotationElement extends AnnotationElement { ? "transparent" : Util.makeHexColor(color[0], color[1], color[2]); } - - _dispatchEventFromSandbox(actions, jsEvent) { - const setColor = (jsName, styleName, event) => { - const color = event.detail[jsName]; - event.target.style[styleName] = ColorConverters[`${color[0]}_HTML`]( - color.slice(1) - ); - }; - - const commonActions = { - display: event => { - const hidden = event.detail.display % 2 === 1; - event.target.style.visibility = hidden ? "hidden" : "visible"; - this.annotationStorage.setValue(this.data.id, { - hidden, - print: event.detail.display === 0 || event.detail.display === 3, - }); - }, - print: event => { - this.annotationStorage.setValue(this.data.id, { - print: event.detail.print, - }); - }, - hidden: event => { - event.target.style.visibility = event.detail.hidden - ? "hidden" - : "visible"; - this.annotationStorage.setValue(this.data.id, { - hidden: event.detail.hidden, - }); - }, - focus: event => { - setTimeout(() => event.target.focus({ preventScroll: false }), 0); - }, - userName: event => { - // tooltip - event.target.title = event.detail.userName; - }, - readonly: event => { - if (event.detail.readonly) { - event.target.setAttribute("readonly", ""); - } else { - event.target.removeAttribute("readonly"); - } - }, - required: event => { - if (event.detail.required) { - event.target.setAttribute("required", ""); - } else { - event.target.removeAttribute("required"); - } - }, - bgColor: event => { - setColor("bgColor", "backgroundColor", event); - }, - fillColor: event => { - setColor("fillColor", "backgroundColor", event); - }, - fgColor: event => { - setColor("fgColor", "color", event); - }, - textColor: event => { - setColor("textColor", "color", event); - }, - borderColor: event => { - setColor("borderColor", "borderColor", event); - }, - strokeColor: event => { - setColor("strokeColor", "borderColor", event); - }, - }; - - for (const name of Object.keys(jsEvent.detail)) { - const action = actions[name] || commonActions[name]; - if (action) { - action(jsEvent); - } - } - } } class TextWidgetAnnotationElement extends WidgetAnnotationElement { @@ -909,12 +934,12 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { // from parsing the elements correctly for the reference tests. const storedData = storage.getValue(id, { value: this.data.fieldValue, - valueAsString: this.data.fieldValue, }); - const textContent = storedData.valueAsString || storedData.value || ""; + const textContent = storedData.formattedValue || storedData.value || ""; const elementData = { userValue: null, formattedValue: null, + valueOnFocus: "", }; if (this.data.multiLine) { @@ -944,14 +969,15 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { }); element.addEventListener("resetform", event => { - const defaultValue = this.data.defaultFieldValue || ""; + const defaultValue = this.data.defaultFieldValue ?? ""; element.value = elementData.userValue = defaultValue; - delete elementData.formattedValue; + elementData.formattedValue = null; }); let blurListener = event => { - if (elementData.formattedValue) { - event.target.value = elementData.formattedValue; + const { formattedValue } = elementData; + if (formattedValue !== null && formattedValue !== undefined) { + event.target.value = formattedValue; } // Reset the cursor position to the start of the field (issue 12359). event.target.scrollLeft = 0; @@ -962,32 +988,33 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { if (elementData.userValue) { event.target.value = elementData.userValue; } + elementData.valueOnFocus = event.target.value; }); element.addEventListener("updatefromsandbox", jsEvent => { const actions = { value(event) { - elementData.userValue = event.detail.value || ""; + elementData.userValue = event.detail.value ?? ""; storage.setValue(id, { value: elementData.userValue.toString() }); - if (!elementData.formattedValue) { - event.target.value = elementData.userValue; - } + event.target.value = elementData.userValue; }, - valueAsString(event) { - elementData.formattedValue = event.detail.valueAsString || ""; - if (event.target !== document.activeElement) { + formattedValue(event) { + const { formattedValue } = event.detail; + elementData.formattedValue = formattedValue; + if ( + formattedValue !== null && + formattedValue !== undefined && + event.target !== document.activeElement + ) { // Input hasn't the focus so display formatted string - event.target.value = elementData.formattedValue; + event.target.value = formattedValue; } storage.setValue(id, { - formattedValue: elementData.formattedValue, + formattedValue, }); }, selRange(event) { - const [selStart, selEnd] = event.detail.selRange; - if (selStart >= 0 && selEnd < event.target.value.length) { - event.target.setSelectionRange(selStart, selEnd); - } + event.target.setSelectionRange(...event.detail.selRange); }, }; this._dispatchEventFromSandbox(actions, jsEvent); @@ -1009,14 +1036,18 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { if (commitKey === -1) { return; } + const { value } = event.target; + if (elementData.valueOnFocus === value) { + return; + } // Save the entered value - elementData.userValue = event.target.value; + elementData.userValue = value; this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { source: this, detail: { id, name: "Keystroke", - value: event.target.value, + value, willCommit: true, commitKey, selStart: event.target.selectionStart, @@ -1027,15 +1058,16 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { const _blurListener = blurListener; blurListener = null; element.addEventListener("blur", event => { - elementData.userValue = event.target.value; - if (this._mouseState.isDown) { + const { value } = event.target; + elementData.userValue = value; + if (this._mouseState.isDown && elementData.valueOnFocus !== value) { // Focus out using the mouse: data are committed this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { source: this, detail: { id, name: "Keystroke", - value: event.target.value, + value, willCommit: true, commitKey: 1, selStart: event.target.selectionStart, @@ -1048,19 +1080,56 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { if (this.data.actions?.Keystroke) { element.addEventListener("beforeinput", event => { - elementData.formattedValue = ""; const { data, target } = event; const { value, selectionStart, selectionEnd } = target; + + let selStart = selectionStart, + selEnd = selectionEnd; + + switch (event.inputType) { + // https://rawgit.com/w3c/input-events/v1/index.html#interface-InputEvent-Attributes + case "deleteWordBackward": { + const match = value + .substring(0, selectionStart) + .match(/\w*[^\w]*$/); + if (match) { + selStart -= match[0].length; + } + break; + } + case "deleteWordForward": { + const match = value + .substring(selectionStart) + .match(/^[^\w]*\w*/); + if (match) { + selEnd += match[0].length; + } + break; + } + case "deleteContentBackward": + if (selectionStart === selectionEnd) { + selStart -= 1; + } + break; + case "deleteContentForward": + if (selectionStart === selectionEnd) { + selEnd += 1; + } + break; + } + + // We handle the event ourselves. + event.preventDefault(); this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { source: this, detail: { id, name: "Keystroke", value, - change: data, + change: data || "", willCommit: false, - selStart: selectionStart, - selEnd: selectionEnd, + selStart, + selEnd, }, }); }); @@ -1104,6 +1173,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { this._setTextStyle(element); this._setBackgroundColor(element); + this._setDefaultPropertiesFromJS(element); this.container.appendChild(element); return this.container; @@ -1213,6 +1283,7 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement { } this._setBackgroundColor(element); + this._setDefaultPropertiesFromJS(element); this.container.appendChild(element); return this.container; @@ -1300,6 +1371,7 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement { } this._setBackgroundColor(element); + this._setDefaultPropertiesFromJS(element); this.container.appendChild(element); return this.container; @@ -1322,6 +1394,8 @@ class PushButtonWidgetAnnotationElement extends LinkAnnotationElement { container.title = this.data.alternativeText; } + this._setDefaultPropertiesFromJS(container); + return container; } } @@ -1534,6 +1608,7 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { } this._setBackgroundColor(selectElement); + this._setDefaultPropertiesFromJS(selectElement); this.container.appendChild(selectElement); return this.container; diff --git a/src/display/annotation_storage.js b/src/display/annotation_storage.js index def437187fdc4..43099079b62ab 100644 --- a/src/display/annotation_storage.js +++ b/src/display/annotation_storage.js @@ -50,6 +50,18 @@ class AnnotationStorage { return Object.assign(defaultValue, value); } + /** + * Get the value for a given key. + * + * @public + * @memberof AnnotationStorage + * @param {string} key + * @returns {Object} + */ + getRawValue(key) { + return this._storage.get(key); + } + /** * Set the value for a given key * diff --git a/src/scripting_api/app.js b/src/scripting_api/app.js index 46ec5ba36f6f6..9f94aa672a7d7 100644 --- a/src/scripting_api/app.js +++ b/src/scripting_api/app.js @@ -434,7 +434,7 @@ class App extends PDFObject { oDoc = null, oCheckbox = null ) { - if (typeof cMsg === "object") { + if (cMsg && typeof cMsg === "object") { nType = cMsg.nType; cMsg = cMsg.cMsg; } @@ -580,7 +580,7 @@ class App extends PDFObject { } response(cQuestion, cTitle = "", cDefault = "", bPassword = "", cLabel = "") { - if (typeof cQuestion === "object") { + if (cQuestion && typeof cQuestion === "object") { cDefault = cQuestion.cDefault; cQuestion = cQuestion.cQuestion; } @@ -590,7 +590,7 @@ class App extends PDFObject { } setInterval(cExpr, nMilliseconds = 0) { - if (typeof cExpr === "object") { + if (cExpr && typeof cExpr === "object") { nMilliseconds = cExpr.nMilliseconds || 0; cExpr = cExpr.cExpr; } @@ -609,7 +609,7 @@ class App extends PDFObject { } setTimeOut(cExpr, nMilliseconds = 0) { - if (typeof cExpr === "object") { + if (cExpr && typeof cExpr === "object") { nMilliseconds = cExpr.nMilliseconds || 0; cExpr = cExpr.cExpr; } diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js index 905f9cfec33f3..153264393d821 100644 --- a/src/scripting_api/doc.js +++ b/src/scripting_api/doc.js @@ -820,8 +820,8 @@ class Doc extends PDFObject { /* Not implemented */ } - getField(cName) { - if (typeof cName === "object") { + _getField(cName) { + if (cName && typeof cName === "object") { cName = cName.cName; } if (typeof cName !== "string") { @@ -859,6 +859,14 @@ class Doc extends PDFObject { return null; } + getField(cName) { + const field = this._getField(cName); + if (!field) { + return null; + } + return field.wrapped; + } + _getChildren(fieldName) { // Children of foo.bar are foo.bar.oof, foo.bar.rab // but not foo.bar.oof.FOO. @@ -889,7 +897,7 @@ class Doc extends PDFObject { } getNthFieldName(nIndex) { - if (typeof nIndex === "object") { + if (nIndex && typeof nIndex === "object") { nIndex = nIndex.nIndex; } if (typeof nIndex !== "number") { @@ -1027,7 +1035,7 @@ class Doc extends PDFObject { bAnnotations = true, printParams = null ) { - if (typeof bUI === "object") { + if (bUI && typeof bUI === "object") { nStart = bUI.nStart; nEnd = bUI.nEnd; bSilent = bUI.bSilent; @@ -1103,30 +1111,52 @@ class Doc extends PDFObject { } resetForm(aFields = null) { - if (aFields && !Array.isArray(aFields) && typeof aFields === "object") { + // Handle the case resetForm({ aFields: ... }) + if (aFields && typeof aFields === "object") { aFields = aFields.aFields; } + + if (aFields && !Array.isArray(aFields)) { + aFields = [aFields]; + } + let mustCalculate = false; + let fieldsToReset; if (aFields) { + fieldsToReset = []; for (const fieldName of aFields) { if (!fieldName) { continue; } - const field = this.getField(fieldName); + if (typeof fieldName !== "string") { + // In Acrobat if a fieldName is not a string all the fields are reset. + fieldsToReset = null; + break; + } + const field = this._getField(fieldName); if (!field) { continue; } - field.value = field.defaultValue; - field.valueAsString = field.value; + fieldsToReset.push(field); mustCalculate = true; } - } else { + } + + if (!fieldsToReset) { + fieldsToReset = this._fields.values(); mustCalculate = this._fields.size !== 0; - for (const field of this._fields.values()) { - field.value = field.defaultValue; - field.valueAsString = field.value; - } } + + for (const field of fieldsToReset) { + field.obj.value = field.obj.defaultValue; + this._send({ + id: field.obj._id, + value: field.obj.defaultValue, + formattedValue: null, + selRange: [0, 0], + }); + } + if (mustCalculate) { this.calculateNow(); } diff --git a/src/scripting_api/event.js b/src/scripting_api/event.js index 71591b6f3c067..c57f3ddae15a5 100644 --- a/src/scripting_api/event.js +++ b/src/scripting_api/event.js @@ -45,6 +45,7 @@ class EventDispatcher { this._objects = objects; this._document.obj._eventDispatcher = this; + this._isCalculating = false; } mergeChange(event) { @@ -129,61 +130,84 @@ class EventDispatcher { return; case "Action": this.runActions(source, source, event, name); - if (this._document.obj.calculate) { - this.runCalculate(source, event); - } + this.runCalculate(source, event); return; } this.runActions(source, source, event, name); - if (name === "Keystroke") { - if (event.rc) { - if (event.willCommit) { - this.runValidation(source, event); - } else if ( - event.change !== savedChange.change || + if (name !== "Keystroke") { + return; + } + + if (event.rc) { + if (event.willCommit) { + this.runValidation(source, event); + } else { + const value = (source.obj.value = this.mergeChange(event)); + let selStart, selEnd; + if ( event.selStart !== savedChange.selStart || event.selEnd !== savedChange.selEnd ) { - source.wrapped.value = this.mergeChange(event); + // Selection has been changed by the script so apply the changes. + selStart = event.selStart; + selEnd = event.selEnd; + } else { + selEnd = selStart = savedChange.selStart + event.change.length; } - } else if (!event.willCommit) { source.obj._send({ id: source.obj._id, - value: savedChange.value, - selRange: [savedChange.selStart, savedChange.selEnd], - }); - } else { - // Entry is not valid (rc == false) and it's a commit - // so just clear the field. - source.obj._send({ - id: source.obj._id, - value: "", - selRange: [0, 0], + value, + selRange: [selStart, selEnd], }); } + } else if (!event.willCommit) { + source.obj._send({ + id: source.obj._id, + value: savedChange.value, + selRange: [savedChange.selStart, savedChange.selEnd], + }); + } else { + // Entry is not valid (rc == false) and it's a commit + // so just clear the field. + source.obj._send({ + id: source.obj._id, + value: "", + formattedValue: null, + selRange: [0, 0], + }); } } runValidation(source, event) { - const hasRan = this.runActions(source, source, event, "Validate"); + const didValidateRun = this.runActions(source, source, event, "Validate"); if (event.rc) { - if (hasRan) { - source.wrapped.value = event.value; - source.wrapped.valueAsString = event.value; - } else { - source.obj.value = event.value; - source.obj.valueAsString = event.value; - } + source.obj.value = event.value; - if (this._document.obj.calculate) { - this.runCalculate(source, event); + this.runCalculate(source, event); + + const savedValue = (event.value = source.obj.value); + let formattedValue = null; + + if (this.runActions(source, source, event, "Format")) { + formattedValue = event.value; } - event.value = source.obj.value; - this.runActions(source, source, event, "Format"); - source.wrapped.valueAsString = event.value; + source.obj._send({ + id: source.obj._id, + value: savedValue, + formattedValue, + }); + event.value = savedValue; + } else if (didValidateRun) { + // The value is not valid. + source.obj._send({ + id: source.obj._id, + value: "", + formattedValue: null, + selRange: [0, 0], + }); } } @@ -198,17 +222,42 @@ class EventDispatcher { } calculateNow() { - if (!this._calculationOrder) { + // This function can be called by a JS script (doc.calculateNow()). + // If !this._calculationOrder then there is nothing to calculate. + // _isCalculating is here to prevent infinite recursion with calculateNow. + // If !this._document.obj.calculate then the script doesn't want to have + // a calculate. + + if ( + !this._calculationOrder || + this._isCalculating || + !this._document.obj.calculate + ) { return; } + this._isCalculating = true; const first = this._calculationOrder[0]; const source = this._objects[first]; globalThis.event = new Event({}); - this.runCalculate(source, globalThis.event); + + try { + this.runCalculate(source, globalThis.event); + } catch (error) { + this._isCalculating = false; + throw error; + } + + this._isCalculating = false; } runCalculate(source, event) { - if (!this._calculationOrder) { + // _document.obj.calculate is equivalent to doc.calculate and can be + // changed by a script to allow a future calculate or not. + // This function is either called by calculateNow or when an action + // is triggered (in this case we cannot be currently calculating). + // So there are no need to check for _isCalculating because it has + // been already done in calculateNow. + if (!this._calculationOrder || !this._document.obj.calculate) { return; } @@ -218,31 +267,43 @@ class EventDispatcher { } if (!this._document.obj.calculate) { - // An action may have changed calculate value. - continue; + // An action could have changed calculate value. + break; } event.value = null; const target = this._objects[targetId]; + let savedValue = target.obj.value; this.runActions(source, target, event, "Calculate"); if (!event.rc) { continue; } + if (event.value !== null) { - target.wrapped.value = event.value; + // A new value has been calculated so set it. + target.obj.value = event.value; } event.value = target.obj.value; this.runActions(target, target, event, "Validate"); if (!event.rc) { + if (target.obj.value !== savedValue) { + target.wrapped.value = savedValue; + } continue; } - event.value = target.obj.value; - this.runActions(target, target, event, "Format"); - if (event.value !== null) { - target.wrapped.valueAsString = event.value; + savedValue = event.value = target.obj.value; + let formattedValue = null; + if (this.runActions(target, target, event, "Format")) { + formattedValue = event.value; } + + target.obj._send({ + id: target.obj._id, + value: savedValue, + formattedValue, + }); } } } diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index 1351054adac85..3c9e957cdec85 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -87,8 +87,6 @@ class Field extends PDFObject { this._globalEval = data.globalEval; this._appObjects = data.appObjects; - - this.valueAsString = data.valueAsString || this._value; } get currentValueIndices() { @@ -252,14 +250,11 @@ class Field extends PDFObject { } get valueAsString() { - if (this._valueAsString === undefined) { - this._valueAsString = this._value ? this._value.toString() : ""; - } - return this._valueAsString; + return (this._value ?? "").toString(); } - set valueAsString(val) { - this._valueAsString = val ? val.toString() : ""; + set valueAsString(_) { + // Do nothing. } browseForFileToSubmit() { @@ -376,7 +371,9 @@ class Field extends PDFObject { } if (this._children === null) { - this._children = this._document.obj._getChildren(this._fieldPath); + this._children = this._document.obj + ._getChildren(this._fieldPath) + .map(child => child.wrapped); } return this._children; } @@ -481,7 +478,7 @@ class Field extends PDFObject { } _reset() { - this.value = this.valueAsString = this.defaultValue; + this.value = this.defaultValue; } _runActions(event) { diff --git a/src/scripting_api/initialization.js b/src/scripting_api/initialization.js index ad33b86a3fc42..7355d58c5a281 100644 --- a/src/scripting_api/initialization.js +++ b/src/scripting_api/initialization.js @@ -120,8 +120,8 @@ function initSandbox(params) { } const wrapped = new Proxy(field, proxyHandler); - doc._addField(name, wrapped); const _object = { obj: field, wrapped }; + doc._addField(name, _object); for (const object of objs) { appObjects[object.id] = _object; } diff --git a/test/integration/scripting_spec.js b/test/integration/scripting_spec.js index e7cdb9b62da91..d7f7f65b2a3bc 100644 --- a/test/integration/scripting_spec.js +++ b/test/integration/scripting_spec.js @@ -237,7 +237,7 @@ describe("Interaction", () => { await page.click("[data-annotation-id='402R']"); await Promise.all( - ["16", "22", "19", "05", "27"].map(id => + ["16", "22", "19", "05"].map(id => page.waitForFunction( `document.querySelector("#\\\\34 ${id}R").value === ""` ) @@ -256,11 +256,14 @@ describe("Interaction", () => { text = await page.$eval("#\\34 05R", el => el.value); expect(text).toEqual(""); - const sum = await page.$eval("#\\34 27R", el => el.value); - expect(sum).toEqual(""); - checked = await page.$eval("#\\34 49R", el => el.checked); expect(checked).toEqual(false); + + const visibility = await page.$eval( + "#\\34 27R", + el => getComputedStyle(el).visibility + ); + expect(visibility).toEqual("hidden"); }) ); }); @@ -992,7 +995,7 @@ describe("Interaction", () => { await clearInput(page, "#\\33 0R"); await page.focus("#\\32 9R"); - await page.type("#\\32 9R", "12A"); + await page.type("#\\32 9R", "12A", { delay: 100 }); await page.waitForFunction( `document.querySelector("#\\\\32 9R").value !== "12A"` ); @@ -1001,7 +1004,7 @@ describe("Interaction", () => { expect(text).withContext(`In ${browserName}`).toEqual("12"); await page.focus("#\\32 9R"); - await page.type("#\\32 9R", "34"); + await page.type("#\\32 9R", "34", { delay: 100 }); await page.click("[data-annotation-id='30R']"); await page.waitForFunction( @@ -1012,7 +1015,7 @@ describe("Interaction", () => { expect(text).withContext(`In ${browserName}`).toEqual(""); await page.focus("#\\32 9R"); - await page.type("#\\32 9R", "12345"); + await page.type("#\\32 9R", "12345", { delay: 100 }); await page.click("[data-annotation-id='30R']"); text = await page.$eval(`#\\32 9R`, el => el.value); @@ -1049,7 +1052,7 @@ describe("Interaction", () => { await clearInput(page, "#\\33 0R"); await page.focus("#\\33 0R"); - await page.type("#\\33 0R", "(123) 456A"); + await page.type("#\\33 0R", "(123) 456A", { delay: 100 }); await page.waitForFunction( `document.querySelector("#\\\\33 0R").value !== "(123) 456A"` ); @@ -1058,7 +1061,7 @@ describe("Interaction", () => { expect(text).withContext(`In ${browserName}`).toEqual("(123) 456"); await page.focus("#\\33 0R"); - await page.type("#\\33 0R", "-789"); + await page.type("#\\33 0R", "-789", { delay: 100 }); await page.click("[data-annotation-id='29R']"); await page.waitForFunction( @@ -1069,7 +1072,7 @@ describe("Interaction", () => { expect(text).withContext(`In ${browserName}`).toEqual(""); await page.focus("#\\33 0R"); - await page.type("#\\33 0R", "(123) 456-7890"); + await page.type("#\\33 0R", "(123) 456-7890", { delay: 100 }); await page.click("[data-annotation-id='29R']"); text = await page.$eval(`#\\33 0R`, el => el.value); @@ -1108,7 +1111,7 @@ describe("Interaction", () => { await clearInput(page, "#\\33 0R"); await page.focus("#\\33 0R"); - await page.type("#\\33 0R", "123A"); + await page.type("#\\33 0R", "123A", { delay: 100 }); await page.waitForFunction( `document.querySelector("#\\\\33 0R").value !== "123A"` ); @@ -1117,7 +1120,7 @@ describe("Interaction", () => { expect(text).withContext(`In ${browserName}`).toEqual("123"); await page.focus("#\\33 0R"); - await page.type("#\\33 0R", "-456"); + await page.type("#\\33 0R", "-456", { delay: 100 }); await page.click("[data-annotation-id='29R']"); await page.waitForFunction( @@ -1128,7 +1131,7 @@ describe("Interaction", () => { expect(text).withContext(`In ${browserName}`).toEqual(""); await page.focus("#\\33 0R"); - await page.type("#\\33 0R", "123-4567"); + await page.type("#\\33 0R", "123-4567", { delay: 100 }); await page.click("[data-annotation-id='29R']"); text = await page.$eval(`#\\33 0R`, el => el.value); @@ -1137,4 +1140,166 @@ describe("Interaction", () => { ); }); }); + + describe("in issue14862.pdf", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("issue14862.pdf", "#\\32 7R"); + pages.map(async ([, page]) => { + page.on("dialog", async dialog => { + await dialog.dismiss(); + }); + }); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must convert input in uppercase", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + + await page.type("#\\32 7R", "Hello", { delay: 100 }); + await page.waitForFunction( + `document.querySelector("#\\\\32 7R").value !== "Hello"` + ); + + let text = await page.$eval("#\\32 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("HELLO"); + + await page.type("#\\32 7R", " world", { delay: 100 }); + await page.waitForFunction( + `document.querySelector("#\\\\32 7R").value !== "HELLO world"` + ); + + text = await page.$eval("#\\32 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("HELLO WORLD"); + + await page.keyboard.press("Backspace"); + await page.keyboard.press("Backspace"); + + await page.waitForFunction( + `document.querySelector("#\\\\32 7R").value !== "HELLO WORLD"` + ); + + text = await page.$eval("#\\32 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("HELLO WOR"); + + await page.type("#\\32 7R", "12.dL", { delay: 100 }); + + await page.waitForFunction( + `document.querySelector("#\\\\32 7R").value !== "HELLO WOR"` + ); + + text = await page.$eval("#\\32 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("HELLO WORDL"); + + await page.type("#\\32 7R", " ", { delay: 100 }); + + await page.keyboard.down("Control"); + await page.keyboard.press("Backspace"); + await page.keyboard.up("Control"); + + await page.waitForFunction( + `document.querySelector("#\\\\32 7R").value !== "HELLO WORDL "` + ); + + text = await page.$eval("#\\32 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("HELLO "); + + await page.$eval("#\\32 7R", el => { + // Select LL + el.selectionStart = 2; + el.selectionEnd = 4; + }); + + await page.keyboard.press("a"); + text = await page.$eval("#\\32 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("HEAO "); + }) + ); + }); + + it("must check that an infinite loop is not triggered", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + + await page.type("#\\32 8R", "Hello", { delay: 100 }); + await page.waitForFunction( + `document.querySelector("#\\\\32 8R").value !== "123"` + ); + + let text = await page.$eval("#\\32 8R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("Hello123"); + + // The action will trigger a calculateNow which itself + // will trigger a resetForm (inducing a calculateNow) and a + // calculateNow. + await page.click("[data-annotation-id='31R']"); + + await page.waitForFunction( + `document.querySelector("#\\\\32 8R").value !== "Hello123"` + ); + + // Without preventing against infinite loop the field is empty. + text = await page.$eval("#\\32 8R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("123"); + }) + ); + }); + }); + + describe("in issue14705.pdf", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("issue14705.pdf", "#\\32 9R"); + pages.map(async ([, page]) => { + page.on("dialog", async dialog => { + await dialog.dismiss(); + }); + }); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that field value is correctly updated", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + + await page.type("#\\32 9R", "Hello World", { delay: 100 }); + await page.click("#\\32 7R"); + + await page.waitForFunction( + `document.querySelector("#\\\\32 9R").value !== "Hello World"` + ); + + let text = await page.$eval("#\\32 9R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("checked"); + + await page.click("#\\32 7R"); + + await page.waitForFunction( + `document.querySelector("#\\\\32 9R").value !== "checked"` + ); + + text = await page.$eval("#\\32 9R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("unchecked"); + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index c8511d9c65b1c..2dc4fcd79d082 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -520,3 +520,5 @@ !issue14502.pdf !issue13211.pdf !issue14627.pdf +!issue14862.pdf +!issue14705.pdf diff --git a/test/pdfs/issue14705.pdf b/test/pdfs/issue14705.pdf new file mode 100644 index 0000000000000..09220a70eaa53 Binary files /dev/null and b/test/pdfs/issue14705.pdf differ diff --git a/test/pdfs/issue14862.pdf b/test/pdfs/issue14862.pdf new file mode 100755 index 0000000000000..be745fab0e3c5 Binary files /dev/null and b/test/pdfs/issue14862.pdf differ diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index e0127e89f215e..ca4483ec36121 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -1320,7 +1320,7 @@ describe("api", function () { { id: "25R", value: "", - defaultValue: null, + defaultValue: "", multiline: false, password: false, charLimit: null, diff --git a/test/unit/scripting_spec.js b/test/unit/scripting_spec.js index 72af2150b5ebf..0b1763b9e65b7 100644 --- a/test/unit/scripting_spec.js +++ b/test/unit/scripting_spec.js @@ -89,7 +89,7 @@ describe("Scripting", function () { return s; } const number = 123; - const expected = (((number - 1) * number) / 2).toString(); + const expected = ((number - 1) * number) / 2; const refId = getId(); const data = { @@ -120,7 +120,8 @@ describe("Scripting", function () { expect(send_queue.has(refId)).toEqual(true); expect(send_queue.get(refId)).toEqual({ id: refId, - valueAsString: expected, + value: expected, + formattedValue: null, }); }); }); @@ -406,6 +407,7 @@ describe("Scripting", function () { expect(send_queue.get(refId)).toEqual({ id: refId, value: "hella", + selRange: [5, 5], }); }); @@ -478,7 +480,7 @@ describe("Scripting", function () { expect(send_queue.get(refId1)).toEqual({ id: refId1, value: "world", - valueAsString: "world", + formattedValue: null, }); }); }); @@ -799,7 +801,7 @@ describe("Scripting", function () { expect(send_queue.get(refId)).toEqual({ id: refId, value: "123456.789", - valueAsString: "123456.789", + formattedValue: null, }); }); @@ -978,7 +980,7 @@ describe("Scripting", function () { expect(send_queue.get(refId)).toEqual({ id: refId, value: "321", - valueAsString: "321", + formattedValue: null, }); }); @@ -1076,7 +1078,7 @@ describe("Scripting", function () { expect(send_queue.get(refIds[3])).toEqual({ id: refIds[3], value: 1, - valueAsString: "1", + formattedValue: null, }); await sandbox.dispatchEventInSandbox({ @@ -1089,7 +1091,7 @@ describe("Scripting", function () { expect(send_queue.get(refIds[3])).toEqual({ id: refIds[3], value: 3, - valueAsString: "3", + formattedValue: null, }); await sandbox.dispatchEventInSandbox({ @@ -1102,7 +1104,7 @@ describe("Scripting", function () { expect(send_queue.get(refIds[3])).toEqual({ id: refIds[3], value: 6, - valueAsString: "6", + formattedValue: null, }); }); }); @@ -1137,7 +1139,8 @@ describe("Scripting", function () { selStart: 0, selEnd: 0, }); - expect(send_queue.has(refId)).toEqual(false); + expect(send_queue.has(refId)).toEqual(true); + send_queue.delete(refId); await sandbox.dispatchEventInSandbox({ id: refId, @@ -1148,7 +1151,8 @@ describe("Scripting", function () { selStart: 1, selEnd: 1, }); - expect(send_queue.has(refId)).toEqual(false); + expect(send_queue.has(refId)).toEqual(true); + send_queue.delete(refId); await sandbox.dispatchEventInSandbox({ id: refId, @@ -1159,7 +1163,8 @@ describe("Scripting", function () { selStart: 2, selEnd: 2, }); - expect(send_queue.has(refId)).toEqual(false); + expect(send_queue.has(refId)).toEqual(true); + send_queue.delete(refId); await sandbox.dispatchEventInSandbox({ id: refId, @@ -1187,7 +1192,8 @@ describe("Scripting", function () { selStart: 3, selEnd: 3, }); - expect(send_queue.has(refId)).toEqual(false); + expect(send_queue.has(refId)).toEqual(true); + send_queue.delete(refId); await sandbox.dispatchEventInSandbox({ id: refId, @@ -1200,7 +1206,8 @@ describe("Scripting", function () { expect(send_queue.has(refId)).toEqual(true); expect(send_queue.get(refId)).toEqual({ id: refId, - valueAsString: "3F?0", + value: "3F?0", + formattedValue: null, }); }); }); @@ -1242,7 +1249,8 @@ describe("Scripting", function () { selStart: i, selEnd: i, }); - expect(send_queue.has(refId)).toEqual(false); + expect(send_queue.has(refId)).toEqual(true); + send_queue.delete(refId); value += change; } @@ -1301,7 +1309,8 @@ describe("Scripting", function () { selStart: i, selEnd: i, }); - expect(send_queue.has(refId)).toEqual(false); + expect(send_queue.has(refId)).toEqual(true); + send_queue.delete(refId); value += change; } @@ -1360,7 +1369,8 @@ describe("Scripting", function () { selStart: i, selEnd: i, }); - expect(send_queue.has(refId)).toEqual(false); + expect(send_queue.has(refId)).toEqual(true); + send_queue.delete(refId); value += change; }