From 016e5fa05ec10356cc45289df6f35de386b65010 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 17 Nov 2020 18:34:44 +0100 Subject: [PATCH] JS -- Add aform functions * These functions aren't in the PDF specs but seems to be widely used * So the specs for these functions are: * http://www.sfu.ca/~wcs/ForGraham/Aladdin%20stuff/Acrobat%20Reader%205.0/Contents/MacOS/JavaScripts/AForm.js * pdfium source code --- src/scripting_api/aform.js | 550 ++++++++++++++++++++++++++++++- test/unit/scripting_spec.js | 632 +++++++++++++++++++++++++++++++++++- 2 files changed, 1174 insertions(+), 8 deletions(-) diff --git a/src/scripting_api/aform.js b/src/scripting_api/aform.js index 618f17f627b116..7b0d07ddca93e2 100644 --- a/src/scripting_api/aform.js +++ b/src/scripting_api/aform.js @@ -13,18 +13,110 @@ * limitations under the License. */ +/* global color */ + class AForm { constructor(document, app, util) { this._document = document; this._app = app; this._util = util; + this._dateFormats = [ + "m/d", + "m/d/yy", + "mm/dd/yy", + "mm/yy", + "d-mmm", + "d-mmm-yy", + "dd-mmm-yy", + "yy-mm-dd", + "mmm-yy", + "mmmm-yy", + "mmm d, yyyy", + "mmmm d, yyyy", + "m/d/yy h:MM tt", + "m/d/yy HH:MM", + ]; + this._timeFormats = ["HH:MM", "h:MM tt", "HH:MM:ss", "h:MM:ss tt"]; + } + + _parseDate(cFormat, cDate) { + const ddate = Date.parse(cDate); + if (isNaN(ddate)) { + try { + return this._util.scand(cFormat, cDate); + } catch (error) { + return null; + } + } else { + return new Date(ddate); + } + } + + AFMergeChange(event) { + if (!event) { + event = this._document._event; + } + if (event.willCommit) { + return event.value.toString(); + } + + return this._app._eventDispatcher.mergeChange(event); + } + + AFParseDateEx(cString, cOrder) { + return this._parseDate(cOrder, cString); + } + + AFExtractNums(str) { + if (typeof str === "number") { + return [str]; + } + if (!str || typeof str !== "string") { + return null; + } + + const first = str.charAt(0); + if (first === "." || first === ",") { + str = `0${str}`; + } + + const numbers = str.match(/([0-9]+)/g); + if (numbers.length === 0) { + return null; + } + + return numbers; + } + + AFMakeNumber(str) { + if (typeof str === "number") { + return str; + } + if (typeof str !== "string") { + return null; + } + + str = str.trim().replace(",", "."); + const number = parseFloat(str); + if (isNaN(number) || !isFinite(number)) { + return null; + } + + return number; + } + + AFMakeArrayFromList(string) { + if (typeof string === "string") { + return string.split(/, ?/g); + } + return string; } AFNumber_Format( nDec, sepStyle, negStyle, - currStyle, + currStyle /* unused */, strCurrency, bCurrencyPrepend ) { @@ -33,13 +125,457 @@ class AForm { return; } - nDec = Math.abs(nDec); - const value = event.value.trim().replace(",", "."); - let number = Number.parseFloat(value); - if (isNaN(number) || !isFinite(number)) { - number = 0; + let value = this.AFMakeNumber(event.value); + if (value === null) { + event.value = ""; + return; + } + + const sign = Math.sign(value); + const buf = []; + let hasParen = false; + + if (sign === -1 && bCurrencyPrepend && negStyle === 0) { + buf.push("-"); + } + + if ((negStyle === 2 || negStyle === 3) && sign === -1) { + buf.push("("); + hasParen = true; + } + + if (bCurrencyPrepend) { + buf.push(strCurrency); + } + + // sepStyle is an integer in [0;4] + sepStyle = Math.min(Math.max(0, Math.floor(sepStyle)), 4); + + buf.push("%,"); + buf.push(sepStyle); + buf.push("."); + buf.push(nDec.toString()); + buf.push("f"); + + if (!bCurrencyPrepend) { + buf.push(strCurrency); + } + + if (hasParen) { + buf.push(")"); + } + + if (negStyle === 1 || negStyle === 3) { + event.target.textColor = sign === 1 ? color.black : color.red; + } + + if ((negStyle !== 0 || bCurrencyPrepend) && sign === -1) { + value = -value; + } + + const formatStr = buf.join(""); + event.value = this._util.printf(formatStr, value); + } + + AFNumber_Keystroke( + nDec /* unused */, + sepStyle, + negStyle /* unused */, + currStyle /* unused */, + strCurrency /* unused */, + bCurrencyPrepend /* unused */ + ) { + const event = this._document._event; + let value = this.AFMergeChange(event); + if (!value) { + return; + } + value = value.trim(); + + let pattern; + if (sepStyle > 1) { + // comma sep + pattern = event.willCommit + ? /^[+-]?([0-9]+(,[0-9]*)?|,[0-9]+)$/ + : /^[+-]?[0-9]*,?[0-9]*$/; + } else { + // dot sep + pattern = event.willCommit + ? /^[+-]?([0-9]+(\.[0-9]*)?|\.[0-9]+)$/ + : /^[+-]?[0-9]*\.?[0-9]*$/; + } + + if (!pattern.test(value)) { + if (event.willCommit) { + if (event.target) { + this._app.alert(`Invalid number in [ ${event.target.name} ]`); + } else { + this._app.alert(`Invalid number`); + } + } + event.rc = false; + } + } + + AFPercent_Format(nDec, sepStyle, percentPrepend = false) { + if (typeof nDec !== "number") { + return; + } + if (typeof sepStyle !== "number") { + return; + } + if (nDec < 0) { + throw new Error("Invalid nDec value in AFPercent_Format"); + } + + const event = this._document._event; + if (nDec > 512) { + event.value = "%"; + return; + } + + nDec = Math.floor(nDec); + + // sepStyle is an integer in [0;4] + sepStyle = Math.min(Math.max(0, Math.floor(sepStyle)), 4); + + let value = this.AFMakeNumber(event.value); + if (value === null) { + event.value = "%"; + return; + } + + const formatStr = `%,${sepStyle}.${nDec}f`; + value = this._util.printf(formatStr, value * 100); + + if (percentPrepend) { + event.value = `%${value}`; + } else { + event.value = `${value}%`; + } + } + + AFPercent_Keystroke(nDec, sepStyle) { + this.AFNumber_Keystroke(nDec, sepStyle, 0, 0, "", true); + } + + AFDate_FormatEx(cFormat) { + const event = this._document._event; + const value = event.value; + if (!value) { + return; + } + + const date = this._parseDate(cFormat, value); + if (date !== null) { + event.value = this._util.printd(cFormat, date); + } + } + + AFDate_Format(pdf) { + if (pdf >= 0 && pdf < this._dateFormats.length) { + this.AFDate_FormatEx(this._dateFormats[pdf]); + } + } + + AFDate_KeystrokeEx(cFormat) { + const event = this._document._event; + if (!event.willCommit) { + return; + } + + const value = event.value; + if (!value) { + return; + } + + if (this._parseDate(cFormat, value) === null) { + this._app.alert("Invalid date"); + event.rc = false; + } + } + + AFDate_Keystroke(pdf) { + if (pdf >= 0 && pdf < this._dateFormats.length) { + this.AFDate_KeystrokeEx(this._dateFormats[pdf]); + } + } + + AFRange_Validate(bGreaterThan, nGreaterThan, bLessThan, nLessThan) { + const event = this._document._event; + if (!event.value) { + return; + } + + const value = this.AFMakeNumber(event.value); + if (value === null) { + return; + } + + bGreaterThan = !!bGreaterThan; + bLessThan = !!bLessThan; + + if (bGreaterThan) { + nGreaterThan = this.AFMakeNumber(nGreaterThan); + if (nGreaterThan === null) { + return; + } + } + + if (bLessThan) { + nLessThan = this.AFMakeNumber(nLessThan); + if (nLessThan === null) { + return; + } + } + + let err = ""; + if (bGreaterThan && bLessThan) { + if (value < nGreaterThan || value > nLessThan) { + err = `${event.value} is not between ${nGreaterThan} and ${nLessThan}`; + } + } else if (bGreaterThan) { + if (value < nGreaterThan) { + err = `${event.value} is not greater or equal than ${nGreaterThan}`; + } + } else if (value > nLessThan) { + err = `${event.value} is not less or equal than ${nLessThan}`; + } + if (err) { + this._app.alert(err); + event.rc = false; + } + } + + AFSimple(cFunction, nValue1, nValue2) { + const value1 = this.AFMakeNumber(nValue1); + if (value1 === null) { + throw new Error("Invalid nValue1 in AFSimple"); + } + + const value2 = this.AFMakeNumber(nValue2); + if (value2 === null) { + throw new Error("Invalid nValue2 in AFSimple"); + } + + switch (cFunction) { + case "AVG": + return (value1 + value2) / 2; + case "SUM": + return value1 + value2; + case "PRD": + return value1 * value2; + case "MIN": + return Math.min(value1, value2); + case "MAX": + return Math.max(value1, value2); + } + + throw new Error("Invalid cFunction in AFSimple"); + } + + AFSimple_Calculate(cFunction, cFields) { + const actions = { + AVG: args => args.reduce((acc, value) => acc + value, 0) / args.length, + SUM: args => args.reduce((acc, value) => acc + value, 0), + PRD: args => args.reduce((acc, value) => acc * value, 1), + MIN: args => + args.reduce((acc, value) => Math.min(acc, value), Number.MAX_VALUE), + MAX: args => + args.reduce((acc, value) => Math.max(acc, value), Number.MIN_VALUE), + }; + + if (!(cFunction in actions)) { + throw new TypeError("Invalid function in AFSimple_Calculate"); + } + + const event = this._document._event; + const values = []; + for (const cField of cFields) { + const field = this._document.getField(cField); + const number = this.AFMakeNumber(field.value); + if (number !== null) { + values.push(number); + } + } + + if (values.length === 0) { + event.value = cFunction === "PRD" ? 1 : 0; + return; + } + + const res = actions[cFunction](values); + event.value = Math.round(1e6 * res) / 1e6; + } + + AFSpecial_Format(psf) { + const event = this._document._event; + if (!event.value) { + return; + } + + psf = this.AFMakeNumber(psf); + if (psf === null) { + throw new Error("Invalid psf in AFSpecial_Format"); + } + + let formatStr = ""; + switch (psf) { + case 0: + formatStr = "99999"; + break; + case 1: + formatStr = "99999-9999"; + break; + case 2: + if (this._util.printx("9999999999", event.value).length >= 10) { + formatStr = "(999) 999-9999"; + } else { + formatStr = "999-9999"; + } + break; + case 3: + formatStr = "999-99-9999"; + break; + default: + throw new Error("Invalid psf in AFSpecial_Format"); + } + + event.value = this._util.printx(formatStr, event.value); + } + + AFSpecial_KeystrokeEx(cMask) { + if (!cMask) { + return; + } + + const event = this._document._event; + const value = this.AFMergeChange(event); + const checkers = new Map([ + ["9", char => char >= "0" && char <= "9"], + [ + "A", + char => ("a" <= char && char <= "z") || ("A" <= char && char <= "Z"), + ], + [ + "O", + char => + ("a" <= char && char <= "z") || + ("A" <= char && char <= "Z") || + ("0" <= char && char <= "9"), + ], + ["X", char => true], + ]); + + function _checkValidity(_value, _cMask) { + for (let i = 0, ii = value.length; i < ii; i++) { + const mask = _cMask.charAt(i); + const char = _value.charAt(i); + const checker = checkers.get(mask); + if (checker) { + if (!checker(char)) { + return false; + } + } else if (mask !== char) { + return false; + } + } + return true; + } + + if (!value) { + return; + } + + if (value.length > cMask.length) { + this._app.alert("Value is too long"); + event.rc = false; + return; + } + + if (event.willCommit) { + if (value.length < cMask.length) { + this._app.alert("Value is too short"); + event.rc = false; + return; + } + + if (!_checkValidity(value, cMask)) { + this._app.alert("Value doesn't fit the specified format"); + event.rc = false; + return; + } + return; + } + + if (value.length < cMask.length) { + cMask = cMask.substring(0, value.length); + } + + if (!_checkValidity(value, cMask)) { + event.rc = false; + } + } + + AFSpecial_Keystroke(psf) { + const event = this._document._event; + if (!event.value) { + return; + } + + psf = this.AFMakeNumber(psf); + if (psf === null) { + throw new Error("Invalid psf in AFSpecial_Keystroke"); + } + + let formatStr; + switch (psf) { + case 0: + formatStr = "99999"; + break; + case 1: + formatStr = "99999-9999"; + break; + case 2: + const finalLen = + event.value.length + + event.change.length + + event.selStart - + event.selEnd; + if (finalLen >= 8) { + formatStr = "(999) 999-9999"; + } else { + formatStr = "999-9999"; + } + break; + case 3: + formatStr = "999-99-9999"; + break; + default: + throw new Error("Invalid psf in AFSpecial_Keystroke"); + } + + this.AFSpecial_KeystrokeEx(formatStr); + } + + AFTime_FormatEx(cFormat) { + this.AFDate_FormatEx(cFormat); + } + + AFTime_Format(pdf) { + if (pdf >= 0 && pdf < this._timeFormats.length) { + this.AFDate_FormatEx(this._timeFormats[pdf]); + } + } + + AFTime_KeystrokeEx(cFormat) { + this.AFDate_KeystrokeEx(cFormat); + } + + AFTime_Keystroke(pdf) { + if (pdf >= 0 && pdf < this._timeFormats.length) { + this.AFDate_KeystrokeEx(this._timeFormats[pdf]); } - event.value = number.toFixed(nDec); } } diff --git a/test/unit/scripting_spec.js b/test/unit/scripting_spec.js index e4d6b1077c8a24..1bb2baf08809be 100644 --- a/test/unit/scripting_spec.js +++ b/test/unit/scripting_spec.js @@ -37,7 +37,9 @@ describe("Scripting", function () { ref = 1; send_queue = new Map(); window.dispatchEvent = event => { - if (send_queue.has(event.detail.id)) { + if (event.detail.command) { + send_queue.set(event.detail.command, event.detail); + } else if (send_queue.has(event.detail.id)) { const prev = send_queue.get(event.detail.id); Object.assign(prev, event.detail); } else { @@ -573,4 +575,632 @@ describe("Scripting", function () { ]).then(() => done()); }); }); + + describe("AForm", function () { + beforeAll(function (done) { + sandbox.createSandbox({ + appInfo: { language: "en-US", platform: "Linux x86_64" }, + objects: {}, + calculationOrder: [], + dispatchEventName: "_dispatchMe", + }); + done(); + }); + + describe("AFExtractNums", function () { + it("should extract numbers", function (done) { + Promise.all([ + myeval(`AFExtractNums("123 456 789")`).then(value => { + expect(value).toEqual(["123", "456", "789"]); + }), + myeval(`AFExtractNums("123.456")`).then(value => { + expect(value).toEqual(["123", "456"]); + }), + myeval(`AFExtractNums("123")`).then(value => { + expect(value).toEqual(["123"]); + }), + myeval(`AFExtractNums(".123")`).then(value => { + expect(value).toEqual(["0", "123"]); + }), + myeval(`AFExtractNums(",123")`).then(value => { + expect(value).toEqual(["0", "123"]); + }), + ]).then(() => done()); + }); + }); + + describe("AFMakeNumber", function () { + it("should convert string to number", function (done) { + Promise.all([ + myeval(`AFMakeNumber("123.456")`).then(value => { + expect(value).toEqual(123.456); + }), + myeval(`AFMakeNumber(123.456)`).then(value => { + expect(value).toEqual(123.456); + }), + myeval(`AFMakeNumber("-123.456")`).then(value => { + expect(value).toEqual(-123.456); + }), + myeval(`AFMakeNumber("-123,456")`).then(value => { + expect(value).toEqual(-123.456); + }), + myeval(`AFMakeNumber("not a number")`).then(value => { + expect(value).toEqual(null); + }), + ]).then(() => done()); + }); + }); + + describe("AFMakeArrayFromList", function () { + it("should split a string into an array of strings", async function (done) { + const value = await myeval( + `AFMakeArrayFromList("aaaa, bbbbbbb,cc,ddd, e")` + ); + expect(value).toEqual(["aaaa", " bbbbbbb", "cc", "ddd", "e"]); + done(); + }); + }); + + describe("AFNumber_format", function () { + it("should format a number", async function (done) { + const refId = getId(); + const data = { + objects: { + field: [ + { + id: refId, + value: "", + actions: { + test1: [ + `AFNumber_Format(2, 0, 0, 0, "€", false);` + + `event.source.value = event.value;`, + ], + test2: [ + `AFNumber_Format(1, 3, 0, 0, "$", true);` + + `event.source.value = event.value;`, + ], + test3: [ + `AFNumber_Format(2, 0, 1, 0, "€", false);` + + `event.source.value = event.value;`, + ], + test4: [ + `AFNumber_Format(2, 0, 2, 0, "€", false);` + + `event.source.value = event.value;`, + ], + test5: [ + `AFNumber_Format(2, 0, 3, 0, "€", false);` + + `event.source.value = event.value;`, + ], + }, + type: "text", + }, + ], + }, + appInfo: { language: "en-US", platform: "Linux x86_64" }, + calculationOrder: [], + dispatchEventName: "_dispatchMe", + }; + try { + sandbox.createSandbox(data); + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "123456.789", + name: "test1", + }); + expect(send_queue.has(refId)).toEqual(true); + expect(send_queue.get(refId)).toEqual({ + id: refId, + value: "123,456.79€", + }); + send_queue.delete(refId); + + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "223456.789", + name: "test2", + }); + expect(send_queue.has(refId)).toEqual(true); + expect(send_queue.get(refId)).toEqual({ + id: refId, + value: "$223456,8", + }); + send_queue.delete(refId); + + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "-323456.789", + name: "test3", + }); + expect(send_queue.has(refId)).toEqual(true); + expect(send_queue.get(refId)).toEqual({ + id: refId, + value: "323,456.79€", + textColor: ["RGB", 1, 0, 0], + }); + send_queue.delete(refId); + + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "-423456.789", + name: "test4", + }); + expect(send_queue.has(refId)).toEqual(true); + expect(send_queue.get(refId)).toEqual({ + id: refId, + value: "(423,456.79€)", + }); + send_queue.delete(refId); + + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "-52345.678", + name: "test5", + }); + expect(send_queue.has(refId)).toEqual(true); + expect(send_queue.get(refId)).toEqual({ + id: refId, + value: "(52,345.68€)", + textColor: ["RGB", 1, 0, 0], + }); + + done(); + } catch (_) { + done.fail(); + } + }); + }); + + describe("AFNumber_Keystroke", function () { + it("should validate a number on a keystroke event", async function (done) { + const refId = getId(); + const data = { + objects: { + field: [ + { + id: refId, + value: "", + actions: { + Validate: [ + `AFNumber_Keystroke(null, 0, null, null, null, null);`, + ], + }, + type: "text", + name: "MyField", + }, + ], + }, + appInfo: { language: "en-US", platform: "Linux x86_64" }, + calculationOrder: [], + dispatchEventName: "_dispatchMe", + }; + try { + sandbox.createSandbox(data); + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "123456.789", + name: "Keystroke", + willCommit: true, + }); + expect(send_queue.has(refId)).toEqual(true); + expect(send_queue.get(refId)).toEqual({ + id: refId, + value: "123456.789", + valueAsString: "123456.789", + }); + done(); + } catch (_) { + done.fail(); + } + }); + + it("should not validate a number on a keystroke event", async function (done) { + const refId = getId(); + const data = { + objects: { + field: [ + { + id: refId, + value: "", + actions: { + Validate: [ + `AFNumber_Keystroke(null, 0, null, null, null, null);`, + ], + }, + type: "text", + name: "MyField", + }, + ], + }, + appInfo: { language: "en-US", platform: "Linux x86_64" }, + calculationOrder: [], + dispatchEventName: "_dispatchMe", + }; + try { + sandbox.createSandbox(data); + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "123s456.789", + name: "Keystroke", + willCommit: true, + }); + expect(send_queue.has("alert")).toEqual(true); + expect(send_queue.get("alert")).toEqual({ + command: "alert", + value: "Invalid number in [ MyField ]", + }); + done(); + } catch (_) { + done.fail(); + } + }); + }); + + describe("AFPercent_Format", function () { + it("should format a percentage", async function (done) { + const refId = getId(); + const data = { + objects: { + field: [ + { + id: refId, + value: "", + actions: { + test1: [ + `AFPercent_Format(2, 1, false);` + + `event.source.value = event.value;`, + ], + test2: [ + `AFPercent_Format(2, 1, true);` + + `event.source.value = event.value;`, + ], + }, + type: "text", + }, + ], + }, + appInfo: { language: "en-US", platform: "Linux x86_64" }, + calculationOrder: [], + dispatchEventName: "_dispatchMe", + }; + try { + sandbox.createSandbox(data); + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "0.456789", + name: "test1", + }); + expect(send_queue.has(refId)).toEqual(true); + expect(send_queue.get(refId)).toEqual({ + id: refId, + value: "45.68%", + }); + send_queue.delete(refId); + + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "0.456789", + name: "test2", + }); + expect(send_queue.has(refId)).toEqual(true); + expect(send_queue.get(refId)).toEqual({ + id: refId, + value: "%45.68", + }); + + done(); + } catch (_) { + done.fail(); + } + }); + }); + + describe("AFDate_Format", function () { + it("should format a date", async function (done) { + const refId = getId(); + const data = { + objects: { + field: [ + { + id: refId, + value: "", + actions: { + test1: [`AFDate_Format(0);event.source.value = event.value;`], + test2: [ + `AFDate_Format(12);event.source.value = event.value;`, + ], + }, + type: "text", + }, + ], + }, + appInfo: { language: "en-US", platform: "Linux x86_64" }, + calculationOrder: [], + dispatchEventName: "_dispatchMe", + }; + try { + sandbox.createSandbox(data); + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "Sun Apr 15 2007 03:14:15", + name: "test1", + }); + expect(send_queue.has(refId)).toEqual(true); + expect(send_queue.get(refId)).toEqual({ + id: refId, + value: "4/15", + }); + send_queue.delete(refId); + + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "Sun Apr 15 2007 03:14:15", + name: "test2", + }); + expect(send_queue.has(refId)).toEqual(true); + expect(send_queue.get(refId)).toEqual({ + id: refId, + value: "4/15/07 3:14 am", + }); + + done(); + } catch (_) { + done.fail(); + } + }); + }); + + describe("AFRange_Validate", function () { + it("should validate a number in range [a, b]", async function (done) { + const refId = getId(); + const data = { + objects: { + field: [ + { + id: refId, + value: "", + actions: { + Validate: [`AFRange_Validate(true, 123, true, 456);`], + }, + type: "text", + }, + ], + }, + appInfo: { language: "en-US", platform: "Linux x86_64" }, + calculationOrder: [], + dispatchEventName: "_dispatchMe", + }; + try { + sandbox.createSandbox(data); + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "321", + name: "Keystroke", + willCommit: true, + }); + expect(send_queue.has(refId)).toEqual(true); + expect(send_queue.get(refId)).toEqual({ + id: refId, + value: "321", + valueAsString: "321", + }); + + done(); + } catch (_) { + done.fail(); + } + }); + + it("should invalidate a number out of range [a, b]", async function (done) { + const refId = getId(); + const data = { + objects: { + field: [ + { + id: refId, + value: "", + actions: { + Validate: [`AFRange_Validate(true, 123, true, 456);`], + }, + type: "text", + }, + ], + }, + appInfo: { language: "en-US", platform: "Linux x86_64" }, + calculationOrder: [], + dispatchEventName: "_dispatchMe", + }; + try { + sandbox.createSandbox(data); + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "12", + name: "Keystroke", + willCommit: true, + }); + expect(send_queue.has("alert")).toEqual(true); + expect(send_queue.get("alert")).toEqual({ + command: "alert", + value: "12 is not between 123 and 456", + }); + + done(); + } catch (_) { + done.fail(); + } + }); + }); + + describe("ASSimple_Calculate", function () { + it("should compute the sum of several fields", async function (done) { + const refIds = [0, 1, 2, 3].map(_ => getId()); + const data = { + objects: { + field1: [ + { + id: refIds[0], + value: "", + actions: {}, + type: "text", + }, + ], + field2: [ + { + id: refIds[1], + value: "", + actions: {}, + type: "text", + }, + ], + field3: [ + { + id: refIds[2], + value: "", + actions: {}, + type: "text", + }, + ], + field4: [ + { + id: refIds[3], + value: "", + actions: { + Calculate: [ + `AFSimple_Calculate("SUM", ["field1", "field2", "field3"]);`, + ], + }, + type: "text", + }, + ], + }, + appInfo: { language: "en-US", platform: "Linux x86_64" }, + calculationOrder: [refIds[3]], + dispatchEventName: "_dispatchMe", + }; + + try { + sandbox.createSandbox(data); + await sandbox.dispatchEventInSandbox({ + id: refIds[0], + value: "1", + name: "Keystroke", + willCommit: true, + }); + expect(send_queue.has(refIds[3])).toEqual(true); + expect(send_queue.get(refIds[3])).toEqual({ + id: refIds[3], + value: 1, + valueAsString: 1, + }); + + await sandbox.dispatchEventInSandbox({ + id: refIds[1], + value: "2", + name: "Keystroke", + willCommit: true, + }); + expect(send_queue.has(refIds[3])).toEqual(true); + expect(send_queue.get(refIds[3])).toEqual({ + id: refIds[3], + value: 3, + valueAsString: 3, + }); + + await sandbox.dispatchEventInSandbox({ + id: refIds[2], + value: "3", + name: "Keystroke", + willCommit: true, + }); + expect(send_queue.has(refIds[3])).toEqual(true); + expect(send_queue.get(refIds[3])).toEqual({ + id: refIds[3], + value: 6, + valueAsString: 6, + }); + + done(); + } catch (_) { + done.fail(); + } + }); + }); + + describe("AFSpecial_KeystrokeEx", function () { + it("should validate a phone number on a keystroke event", async function (done) { + const refId = getId(); + const data = { + objects: { + field: [ + { + id: refId, + value: "", + actions: { + Keystroke: [`AFSpecial_KeystrokeEx("9AXO");`], + }, + type: "text", + }, + ], + }, + appInfo: { language: "en-US", platform: "Linux x86_64" }, + calculationOrder: [], + dispatchEventName: "_dispatchMe", + }; + try { + sandbox.createSandbox(data); + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "", + change: "3", + name: "Keystroke", + willCommit: false, + selStart: 0, + selEnd: 0, + }); + expect(send_queue.has(refId)).toEqual(false); + + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "3", + change: "F", + name: "Keystroke", + willCommit: false, + selStart: 1, + selEnd: 1, + }); + expect(send_queue.has(refId)).toEqual(false); + + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "3F", + change: "?", + name: "Keystroke", + willCommit: false, + selStart: 2, + selEnd: 2, + }); + expect(send_queue.has(refId)).toEqual(false); + + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "3F?", + change: "@", + name: "Keystroke", + willCommit: false, + selStart: 3, + selEnd: 3, + }); + expect(send_queue.has(refId)).toEqual(true); + expect(send_queue.get(refId)).toEqual({ + id: refId, + value: "3F?", + selRange: [3, 3], + }); + + done(); + } catch (_) { + done.fail(); + } + }); + }); + }); });