2
0

ui_utils_spec.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. /**
  2. * @licstart The following is the entire license notice for the
  3. * JavaScript code in this page
  4. *
  5. * Copyright 2022 Mozilla Foundation
  6. *
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. *
  19. * @licend The above is the entire license notice for the
  20. * JavaScript code in this page
  21. */
  22. "use strict";
  23. var _ui_utils = require("../../web/ui_utils.js");
  24. describe("ui_utils", function () {
  25. describe("binary search", function () {
  26. function isTrue(boolean) {
  27. return boolean;
  28. }
  29. function isGreater3(number) {
  30. return number > 3;
  31. }
  32. it("empty array", function () {
  33. expect((0, _ui_utils.binarySearchFirstItem)([], isTrue)).toEqual(0);
  34. });
  35. it("single boolean entry", function () {
  36. expect((0, _ui_utils.binarySearchFirstItem)([false], isTrue)).toEqual(1);
  37. expect((0, _ui_utils.binarySearchFirstItem)([true], isTrue)).toEqual(0);
  38. });
  39. it("three boolean entries", function () {
  40. expect((0, _ui_utils.binarySearchFirstItem)([true, true, true], isTrue)).toEqual(0);
  41. expect((0, _ui_utils.binarySearchFirstItem)([false, true, true], isTrue)).toEqual(1);
  42. expect((0, _ui_utils.binarySearchFirstItem)([false, false, true], isTrue)).toEqual(2);
  43. expect((0, _ui_utils.binarySearchFirstItem)([false, false, false], isTrue)).toEqual(3);
  44. });
  45. it("three numeric entries", function () {
  46. expect((0, _ui_utils.binarySearchFirstItem)([0, 1, 2], isGreater3)).toEqual(3);
  47. expect((0, _ui_utils.binarySearchFirstItem)([2, 3, 4], isGreater3)).toEqual(2);
  48. expect((0, _ui_utils.binarySearchFirstItem)([4, 5, 6], isGreater3)).toEqual(0);
  49. });
  50. it("three numeric entries and a start index", function () {
  51. expect((0, _ui_utils.binarySearchFirstItem)([0, 1, 2, 3, 4], isGreater3, 2)).toEqual(4);
  52. expect((0, _ui_utils.binarySearchFirstItem)([2, 3, 4], isGreater3, 2)).toEqual(2);
  53. expect((0, _ui_utils.binarySearchFirstItem)([4, 5, 6], isGreater3, 1)).toEqual(1);
  54. });
  55. });
  56. describe("isValidRotation", function () {
  57. it("should reject non-integer angles", function () {
  58. expect((0, _ui_utils.isValidRotation)()).toEqual(false);
  59. expect((0, _ui_utils.isValidRotation)(null)).toEqual(false);
  60. expect((0, _ui_utils.isValidRotation)(NaN)).toEqual(false);
  61. expect((0, _ui_utils.isValidRotation)([90])).toEqual(false);
  62. expect((0, _ui_utils.isValidRotation)("90")).toEqual(false);
  63. expect((0, _ui_utils.isValidRotation)(90.5)).toEqual(false);
  64. });
  65. it("should reject non-multiple of 90 degree angles", function () {
  66. expect((0, _ui_utils.isValidRotation)(45)).toEqual(false);
  67. expect((0, _ui_utils.isValidRotation)(-123)).toEqual(false);
  68. });
  69. it("should accept valid angles", function () {
  70. expect((0, _ui_utils.isValidRotation)(0)).toEqual(true);
  71. expect((0, _ui_utils.isValidRotation)(90)).toEqual(true);
  72. expect((0, _ui_utils.isValidRotation)(-270)).toEqual(true);
  73. expect((0, _ui_utils.isValidRotation)(540)).toEqual(true);
  74. });
  75. });
  76. describe("isPortraitOrientation", function () {
  77. it("should be portrait orientation", function () {
  78. expect((0, _ui_utils.isPortraitOrientation)({
  79. width: 200,
  80. height: 400
  81. })).toEqual(true);
  82. expect((0, _ui_utils.isPortraitOrientation)({
  83. width: 500,
  84. height: 500
  85. })).toEqual(true);
  86. });
  87. it("should be landscape orientation", function () {
  88. expect((0, _ui_utils.isPortraitOrientation)({
  89. width: 600,
  90. height: 300
  91. })).toEqual(false);
  92. });
  93. });
  94. describe("parseQueryString", function () {
  95. it("should parse one key/value pair", function () {
  96. const parameters = (0, _ui_utils.parseQueryString)("key1=value1");
  97. expect(parameters.size).toEqual(1);
  98. expect(parameters.get("key1")).toEqual("value1");
  99. });
  100. it("should parse multiple key/value pairs", function () {
  101. const parameters = (0, _ui_utils.parseQueryString)("key1=value1&key2=value2&key3=value3");
  102. expect(parameters.size).toEqual(3);
  103. expect(parameters.get("key1")).toEqual("value1");
  104. expect(parameters.get("key2")).toEqual("value2");
  105. expect(parameters.get("key3")).toEqual("value3");
  106. });
  107. it("should parse keys without values", function () {
  108. const parameters = (0, _ui_utils.parseQueryString)("key1");
  109. expect(parameters.size).toEqual(1);
  110. expect(parameters.get("key1")).toEqual("");
  111. });
  112. it("should decode encoded key/value pairs", function () {
  113. const parameters = (0, _ui_utils.parseQueryString)("k%C3%ABy1=valu%C3%AB1");
  114. expect(parameters.size).toEqual(1);
  115. expect(parameters.get("këy1")).toEqual("valuë1");
  116. });
  117. it("should convert keys to lowercase", function () {
  118. const parameters = (0, _ui_utils.parseQueryString)("Key1=Value1&KEY2=Value2");
  119. expect(parameters.size).toEqual(2);
  120. expect(parameters.get("key1")).toEqual("Value1");
  121. expect(parameters.get("key2")).toEqual("Value2");
  122. });
  123. });
  124. describe("removeNullCharacters", function () {
  125. it("should not modify string without null characters", function () {
  126. const str = "string without null chars";
  127. expect((0, _ui_utils.removeNullCharacters)(str)).toEqual("string without null chars");
  128. });
  129. it("should modify string with null characters", function () {
  130. const str = "string\x00With\x00Null\x00Chars";
  131. expect((0, _ui_utils.removeNullCharacters)(str)).toEqual("stringWithNullChars");
  132. });
  133. it("should modify string with non-displayable characters", function () {
  134. const str = Array.from(Array(32).keys()).map(x => String.fromCharCode(x) + "a").join("");
  135. const expected = "a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a";
  136. expect((0, _ui_utils.removeNullCharacters)(str, true)).toEqual(expected);
  137. });
  138. });
  139. describe("getPageSizeInches", function () {
  140. it("gets page size (in inches)", function () {
  141. const page = {
  142. view: [0, 0, 595.28, 841.89],
  143. userUnit: 1.0,
  144. rotate: 0
  145. };
  146. const {
  147. width,
  148. height
  149. } = (0, _ui_utils.getPageSizeInches)(page);
  150. expect(+width.toPrecision(3)).toEqual(8.27);
  151. expect(+height.toPrecision(4)).toEqual(11.69);
  152. });
  153. it("gets page size (in inches), for non-default /Rotate entry", function () {
  154. const pdfPage1 = {
  155. view: [0, 0, 612, 792],
  156. userUnit: 1,
  157. rotate: 0
  158. };
  159. const {
  160. width: width1,
  161. height: height1
  162. } = (0, _ui_utils.getPageSizeInches)(pdfPage1);
  163. expect(width1).toEqual(8.5);
  164. expect(height1).toEqual(11);
  165. const pdfPage2 = {
  166. view: [0, 0, 612, 792],
  167. userUnit: 1,
  168. rotate: 90
  169. };
  170. const {
  171. width: width2,
  172. height: height2
  173. } = (0, _ui_utils.getPageSizeInches)(pdfPage2);
  174. expect(width2).toEqual(11);
  175. expect(height2).toEqual(8.5);
  176. });
  177. });
  178. describe("getVisibleElements", function () {
  179. const BORDER_WIDTH = 9;
  180. const SPACING = 2 * BORDER_WIDTH - 7;
  181. function makePages(lines) {
  182. const result = [];
  183. let lineTop = 0,
  184. id = 0;
  185. for (const line of lines) {
  186. const lineHeight = line.reduce(function (maxHeight, pair) {
  187. return Math.max(maxHeight, pair[1]);
  188. }, 0);
  189. let offsetLeft = -BORDER_WIDTH;
  190. for (const [clientWidth, clientHeight] of line) {
  191. const offsetTop = lineTop + (lineHeight - clientHeight) / 2 - BORDER_WIDTH;
  192. const div = {
  193. offsetLeft,
  194. offsetTop,
  195. clientWidth,
  196. clientHeight,
  197. clientLeft: BORDER_WIDTH,
  198. clientTop: BORDER_WIDTH
  199. };
  200. result.push({
  201. id,
  202. div
  203. });
  204. ++id;
  205. offsetLeft += clientWidth + SPACING;
  206. }
  207. lineTop += lineHeight + SPACING;
  208. }
  209. return result;
  210. }
  211. function slowGetVisibleElements(scroll, pages) {
  212. const views = [],
  213. ids = new Set();
  214. const {
  215. scrollLeft,
  216. scrollTop
  217. } = scroll;
  218. const scrollRight = scrollLeft + scroll.clientWidth;
  219. const scrollBottom = scrollTop + scroll.clientHeight;
  220. for (const view of pages) {
  221. const {
  222. div
  223. } = view;
  224. const viewLeft = div.offsetLeft + div.clientLeft;
  225. const viewRight = viewLeft + div.clientWidth;
  226. const viewTop = div.offsetTop + div.clientTop;
  227. const viewBottom = viewTop + div.clientHeight;
  228. if (viewLeft < scrollRight && viewRight > scrollLeft && viewTop < scrollBottom && viewBottom > scrollTop) {
  229. const hiddenHeight = Math.max(0, scrollTop - viewTop) + Math.max(0, viewBottom - scrollBottom);
  230. const hiddenWidth = Math.max(0, scrollLeft - viewLeft) + Math.max(0, viewRight - scrollRight);
  231. const fractionHeight = (div.clientHeight - hiddenHeight) / div.clientHeight;
  232. const fractionWidth = (div.clientWidth - hiddenWidth) / div.clientWidth;
  233. const percent = fractionHeight * fractionWidth * 100 | 0;
  234. views.push({
  235. id: view.id,
  236. x: viewLeft,
  237. y: viewTop,
  238. view,
  239. percent,
  240. widthPercent: fractionWidth * 100 | 0
  241. });
  242. ids.add(view.id);
  243. }
  244. }
  245. return {
  246. first: views[0],
  247. last: views.at(-1),
  248. views,
  249. ids
  250. };
  251. }
  252. function scrollOverDocument(pages, horizontal = false, rtl = false) {
  253. const size = pages.reduce(function (max, {
  254. div
  255. }) {
  256. return Math.max(max, horizontal ? Math.abs(div.offsetLeft + div.clientLeft + div.clientWidth) : div.offsetTop + div.clientTop + div.clientHeight);
  257. }, 0);
  258. for (let i = -size; i < size; i += 7) {
  259. for (let j = i + 5; j < size; j += j - i) {
  260. const scrollEl = horizontal ? {
  261. scrollTop: 0,
  262. scrollLeft: i,
  263. clientHeight: 10000,
  264. clientWidth: j - i
  265. } : {
  266. scrollTop: i,
  267. scrollLeft: 0,
  268. clientHeight: j - i,
  269. clientWidth: 10000
  270. };
  271. expect((0, _ui_utils.getVisibleElements)({
  272. scrollEl,
  273. views: pages,
  274. sortByVisibility: false,
  275. horizontal,
  276. rtl
  277. })).toEqual(slowGetVisibleElements(scrollEl, pages));
  278. }
  279. }
  280. }
  281. it("with pages of varying height", function () {
  282. const pages = makePages([[[50, 20], [20, 50]], [[30, 12], [12, 30]], [[20, 50], [50, 20]], [[50, 20], [20, 50]]]);
  283. scrollOverDocument(pages);
  284. });
  285. it("widescreen challenge", function () {
  286. const pages = makePages([[[10, 50], [10, 60], [10, 70], [10, 80], [10, 90]], [[10, 90], [10, 80], [10, 70], [10, 60], [10, 50]], [[10, 50], [10, 60], [10, 70], [10, 80], [10, 90]]]);
  287. scrollOverDocument(pages);
  288. });
  289. it("works with horizontal scrolling", function () {
  290. const pages = makePages([[[10, 50], [20, 20], [30, 10]]]);
  291. scrollOverDocument(pages, true);
  292. });
  293. it("works with horizontal scrolling with RTL-documents", function () {
  294. const pages = makePages([[[-10, 50], [-20, 20], [-30, 10]]]);
  295. scrollOverDocument(pages, true, true);
  296. });
  297. it("handles `sortByVisibility` correctly", function () {
  298. const scrollEl = {
  299. scrollTop: 75,
  300. scrollLeft: 0,
  301. clientHeight: 750,
  302. clientWidth: 1500
  303. };
  304. const views = makePages([[[100, 150]], [[100, 150]], [[100, 150]]]);
  305. const visible = (0, _ui_utils.getVisibleElements)({
  306. scrollEl,
  307. views
  308. });
  309. const visibleSorted = (0, _ui_utils.getVisibleElements)({
  310. scrollEl,
  311. views,
  312. sortByVisibility: true
  313. });
  314. const viewsOrder = [],
  315. viewsSortedOrder = [];
  316. for (const view of visible.views) {
  317. viewsOrder.push(view.id);
  318. }
  319. for (const view of visibleSorted.views) {
  320. viewsSortedOrder.push(view.id);
  321. }
  322. expect(viewsOrder).toEqual([0, 1, 2]);
  323. expect(viewsSortedOrder).toEqual([1, 2, 0]);
  324. });
  325. it("handles views being empty", function () {
  326. const scrollEl = {
  327. scrollTop: 10,
  328. scrollLeft: 0,
  329. clientHeight: 750,
  330. clientWidth: 1500
  331. };
  332. const views = [];
  333. expect((0, _ui_utils.getVisibleElements)({
  334. scrollEl,
  335. views
  336. })).toEqual({
  337. first: undefined,
  338. last: undefined,
  339. views: [],
  340. ids: new Set()
  341. });
  342. });
  343. it("handles all views being hidden (without errors)", function () {
  344. const scrollEl = {
  345. scrollTop: 100000,
  346. scrollLeft: 0,
  347. clientHeight: 750,
  348. clientWidth: 1500
  349. };
  350. const views = makePages([[[100, 150]], [[100, 150]], [[100, 150]]]);
  351. expect((0, _ui_utils.getVisibleElements)({
  352. scrollEl,
  353. views
  354. })).toEqual({
  355. first: undefined,
  356. last: undefined,
  357. views: [],
  358. ids: new Set()
  359. });
  360. });
  361. describe("backtrackBeforeAllVisibleElements", function () {
  362. const tallPage = [10, 50];
  363. const shortPage = [10, 10];
  364. const top1 = 20 + SPACING + 40;
  365. const top2 = 20 + SPACING + 10;
  366. it("handles case 1", function () {
  367. const pages = makePages([[[10, 20], [10, 20], [10, 20], [10, 20]], [tallPage, shortPage, tallPage, shortPage], [[10, 50], [10, 50], [10, 50], [10, 50]], [[10, 20], [10, 20], [10, 20], [10, 20]], [[10, 20]]]);
  368. const bsResult = 4;
  369. expect((0, _ui_utils.backtrackBeforeAllVisibleElements)(bsResult, pages, top1)).toEqual(4);
  370. });
  371. it("handles case 2", function () {
  372. const pages = makePages([[[10, 20], [10, 20], [10, 20], [10, 20]], [tallPage, shortPage, tallPage, tallPage], [[10, 50], [10, 50], [10, 50], [10, 50]], [[10, 20], [10, 20], [10, 20], [10, 20]]]);
  373. const bsResult = 6;
  374. expect((0, _ui_utils.backtrackBeforeAllVisibleElements)(bsResult, pages, top1)).toEqual(4);
  375. });
  376. it("handles case 3", function () {
  377. const pages = makePages([[[10, 20], [10, 20], [10, 20], [10, 20]], [tallPage, shortPage, tallPage, shortPage], [[10, 50], [10, 50], [10, 50], [10, 50]], [[10, 20], [10, 20], [10, 20], [10, 20]]]);
  378. const bsResult = 8;
  379. expect((0, _ui_utils.backtrackBeforeAllVisibleElements)(bsResult, pages, top1)).toEqual(4);
  380. });
  381. it("handles case 4", function () {
  382. const pages = makePages([[[10, 20], [10, 20], [10, 20], [10, 20]], [tallPage, shortPage, tallPage, shortPage], [[10, 50], [10, 50], [10, 50], [10, 50]], [[10, 20], [10, 20], [10, 20], [10, 20]]]);
  383. const bsResult = 4;
  384. expect((0, _ui_utils.backtrackBeforeAllVisibleElements)(bsResult, pages, top2)).toEqual(4);
  385. });
  386. });
  387. });
  388. });