ui_utils_spec.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. /**
  2. * @licstart The following is the entire license notice for the
  3. * Javascript code in this page
  4. *
  5. * Copyright 2021 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. });
  51. describe("isValidRotation", function () {
  52. it("should reject non-integer angles", function () {
  53. expect((0, _ui_utils.isValidRotation)()).toEqual(false);
  54. expect((0, _ui_utils.isValidRotation)(null)).toEqual(false);
  55. expect((0, _ui_utils.isValidRotation)(NaN)).toEqual(false);
  56. expect((0, _ui_utils.isValidRotation)([90])).toEqual(false);
  57. expect((0, _ui_utils.isValidRotation)("90")).toEqual(false);
  58. expect((0, _ui_utils.isValidRotation)(90.5)).toEqual(false);
  59. });
  60. it("should reject non-multiple of 90 degree angles", function () {
  61. expect((0, _ui_utils.isValidRotation)(45)).toEqual(false);
  62. expect((0, _ui_utils.isValidRotation)(-123)).toEqual(false);
  63. });
  64. it("should accept valid angles", function () {
  65. expect((0, _ui_utils.isValidRotation)(0)).toEqual(true);
  66. expect((0, _ui_utils.isValidRotation)(90)).toEqual(true);
  67. expect((0, _ui_utils.isValidRotation)(-270)).toEqual(true);
  68. expect((0, _ui_utils.isValidRotation)(540)).toEqual(true);
  69. });
  70. });
  71. describe("isPortraitOrientation", function () {
  72. it("should be portrait orientation", function () {
  73. expect((0, _ui_utils.isPortraitOrientation)({
  74. width: 200,
  75. height: 400
  76. })).toEqual(true);
  77. expect((0, _ui_utils.isPortraitOrientation)({
  78. width: 500,
  79. height: 500
  80. })).toEqual(true);
  81. });
  82. it("should be landscape orientation", function () {
  83. expect((0, _ui_utils.isPortraitOrientation)({
  84. width: 600,
  85. height: 300
  86. })).toEqual(false);
  87. });
  88. });
  89. describe("parseQueryString", function () {
  90. it("should parse one key/value pair", function () {
  91. const parameters = (0, _ui_utils.parseQueryString)("key1=value1");
  92. expect(parameters.size).toEqual(1);
  93. expect(parameters.get("key1")).toEqual("value1");
  94. });
  95. it("should parse multiple key/value pairs", function () {
  96. const parameters = (0, _ui_utils.parseQueryString)("key1=value1&key2=value2&key3=value3");
  97. expect(parameters.size).toEqual(3);
  98. expect(parameters.get("key1")).toEqual("value1");
  99. expect(parameters.get("key2")).toEqual("value2");
  100. expect(parameters.get("key3")).toEqual("value3");
  101. });
  102. it("should parse keys without values", function () {
  103. const parameters = (0, _ui_utils.parseQueryString)("key1");
  104. expect(parameters.size).toEqual(1);
  105. expect(parameters.get("key1")).toEqual("");
  106. });
  107. it("should decode encoded key/value pairs", function () {
  108. const parameters = (0, _ui_utils.parseQueryString)("k%C3%ABy1=valu%C3%AB1");
  109. expect(parameters.size).toEqual(1);
  110. expect(parameters.get("këy1")).toEqual("valuë1");
  111. });
  112. it("should convert keys to lowercase", function () {
  113. const parameters = (0, _ui_utils.parseQueryString)("Key1=Value1&KEY2=Value2");
  114. expect(parameters.size).toEqual(2);
  115. expect(parameters.get("key1")).toEqual("Value1");
  116. expect(parameters.get("key2")).toEqual("Value2");
  117. });
  118. });
  119. describe("getPageSizeInches", function () {
  120. it("gets page size (in inches)", function () {
  121. const page = {
  122. view: [0, 0, 595.28, 841.89],
  123. userUnit: 1.0,
  124. rotate: 0
  125. };
  126. const {
  127. width,
  128. height
  129. } = (0, _ui_utils.getPageSizeInches)(page);
  130. expect(+width.toPrecision(3)).toEqual(8.27);
  131. expect(+height.toPrecision(4)).toEqual(11.69);
  132. });
  133. it("gets page size (in inches), for non-default /Rotate entry", function () {
  134. const pdfPage1 = {
  135. view: [0, 0, 612, 792],
  136. userUnit: 1,
  137. rotate: 0
  138. };
  139. const {
  140. width: width1,
  141. height: height1
  142. } = (0, _ui_utils.getPageSizeInches)(pdfPage1);
  143. expect(width1).toEqual(8.5);
  144. expect(height1).toEqual(11);
  145. const pdfPage2 = {
  146. view: [0, 0, 612, 792],
  147. userUnit: 1,
  148. rotate: 90
  149. };
  150. const {
  151. width: width2,
  152. height: height2
  153. } = (0, _ui_utils.getPageSizeInches)(pdfPage2);
  154. expect(width2).toEqual(11);
  155. expect(height2).toEqual(8.5);
  156. });
  157. });
  158. describe("getVisibleElements", function () {
  159. const BORDER_WIDTH = 9;
  160. const SPACING = 2 * BORDER_WIDTH - 7;
  161. function makePages(lines) {
  162. const result = [];
  163. let lineTop = 0,
  164. id = 0;
  165. for (const line of lines) {
  166. const lineHeight = line.reduce(function (maxHeight, pair) {
  167. return Math.max(maxHeight, pair[1]);
  168. }, 0);
  169. let offsetLeft = -BORDER_WIDTH;
  170. for (const [clientWidth, clientHeight] of line) {
  171. const offsetTop = lineTop + (lineHeight - clientHeight) / 2 - BORDER_WIDTH;
  172. const div = {
  173. offsetLeft,
  174. offsetTop,
  175. clientWidth,
  176. clientHeight,
  177. clientLeft: BORDER_WIDTH,
  178. clientTop: BORDER_WIDTH
  179. };
  180. result.push({
  181. id,
  182. div
  183. });
  184. ++id;
  185. offsetLeft += clientWidth + SPACING;
  186. }
  187. lineTop += lineHeight + SPACING;
  188. }
  189. return result;
  190. }
  191. function slowGetVisibleElements(scroll, pages) {
  192. const views = [],
  193. ids = new Set();
  194. const {
  195. scrollLeft,
  196. scrollTop
  197. } = scroll;
  198. const scrollRight = scrollLeft + scroll.clientWidth;
  199. const scrollBottom = scrollTop + scroll.clientHeight;
  200. for (const view of pages) {
  201. const {
  202. div
  203. } = view;
  204. const viewLeft = div.offsetLeft + div.clientLeft;
  205. const viewRight = viewLeft + div.clientWidth;
  206. const viewTop = div.offsetTop + div.clientTop;
  207. const viewBottom = viewTop + div.clientHeight;
  208. if (viewLeft < scrollRight && viewRight > scrollLeft && viewTop < scrollBottom && viewBottom > scrollTop) {
  209. const hiddenHeight = Math.max(0, scrollTop - viewTop) + Math.max(0, viewBottom - scrollBottom);
  210. const hiddenWidth = Math.max(0, scrollLeft - viewLeft) + Math.max(0, viewRight - scrollRight);
  211. const fractionHeight = (div.clientHeight - hiddenHeight) / div.clientHeight;
  212. const fractionWidth = (div.clientWidth - hiddenWidth) / div.clientWidth;
  213. const percent = fractionHeight * fractionWidth * 100 | 0;
  214. views.push({
  215. id: view.id,
  216. x: viewLeft,
  217. y: viewTop,
  218. view,
  219. percent,
  220. widthPercent: fractionWidth * 100 | 0
  221. });
  222. ids.add(view.id);
  223. }
  224. }
  225. return {
  226. first: views[0],
  227. last: views[views.length - 1],
  228. views,
  229. ids
  230. };
  231. }
  232. function scrollOverDocument(pages, horizontal = false, rtl = false) {
  233. const size = pages.reduce(function (max, {
  234. div
  235. }) {
  236. return Math.max(max, horizontal ? Math.abs(div.offsetLeft + div.clientLeft + div.clientWidth) : div.offsetTop + div.clientTop + div.clientHeight);
  237. }, 0);
  238. for (let i = -size; i < size; i += 7) {
  239. for (let j = i + 5; j < size; j += j - i) {
  240. const scrollEl = horizontal ? {
  241. scrollTop: 0,
  242. scrollLeft: i,
  243. clientHeight: 10000,
  244. clientWidth: j - i
  245. } : {
  246. scrollTop: i,
  247. scrollLeft: 0,
  248. clientHeight: j - i,
  249. clientWidth: 10000
  250. };
  251. expect((0, _ui_utils.getVisibleElements)({
  252. scrollEl,
  253. views: pages,
  254. sortByVisibility: false,
  255. horizontal,
  256. rtl
  257. })).toEqual(slowGetVisibleElements(scrollEl, pages));
  258. }
  259. }
  260. }
  261. it("with pages of varying height", function () {
  262. const pages = makePages([[[50, 20], [20, 50]], [[30, 12], [12, 30]], [[20, 50], [50, 20]], [[50, 20], [20, 50]]]);
  263. scrollOverDocument(pages);
  264. });
  265. it("widescreen challenge", function () {
  266. 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]]]);
  267. scrollOverDocument(pages);
  268. });
  269. it("works with horizontal scrolling", function () {
  270. const pages = makePages([[[10, 50], [20, 20], [30, 10]]]);
  271. scrollOverDocument(pages, true);
  272. });
  273. it("works with horizontal scrolling with RTL-documents", function () {
  274. const pages = makePages([[[-10, 50], [-20, 20], [-30, 10]]]);
  275. scrollOverDocument(pages, true, true);
  276. });
  277. it("handles `sortByVisibility` correctly", function () {
  278. const scrollEl = {
  279. scrollTop: 75,
  280. scrollLeft: 0,
  281. clientHeight: 750,
  282. clientWidth: 1500
  283. };
  284. const views = makePages([[[100, 150]], [[100, 150]], [[100, 150]]]);
  285. const visible = (0, _ui_utils.getVisibleElements)({
  286. scrollEl,
  287. views
  288. });
  289. const visibleSorted = (0, _ui_utils.getVisibleElements)({
  290. scrollEl,
  291. views,
  292. sortByVisibility: true
  293. });
  294. const viewsOrder = [],
  295. viewsSortedOrder = [];
  296. for (const view of visible.views) {
  297. viewsOrder.push(view.id);
  298. }
  299. for (const view of visibleSorted.views) {
  300. viewsSortedOrder.push(view.id);
  301. }
  302. expect(viewsOrder).toEqual([0, 1, 2]);
  303. expect(viewsSortedOrder).toEqual([1, 2, 0]);
  304. });
  305. it("handles views being empty", function () {
  306. const scrollEl = {
  307. scrollTop: 10,
  308. scrollLeft: 0,
  309. clientHeight: 750,
  310. clientWidth: 1500
  311. };
  312. const views = [];
  313. expect((0, _ui_utils.getVisibleElements)({
  314. scrollEl,
  315. views
  316. })).toEqual({
  317. first: undefined,
  318. last: undefined,
  319. views: [],
  320. ids: new Set()
  321. });
  322. });
  323. it("handles all views being hidden (without errors)", function () {
  324. const scrollEl = {
  325. scrollTop: 100000,
  326. scrollLeft: 0,
  327. clientHeight: 750,
  328. clientWidth: 1500
  329. };
  330. const views = makePages([[[100, 150]], [[100, 150]], [[100, 150]]]);
  331. expect((0, _ui_utils.getVisibleElements)({
  332. scrollEl,
  333. views
  334. })).toEqual({
  335. first: undefined,
  336. last: undefined,
  337. views: [],
  338. ids: new Set()
  339. });
  340. });
  341. describe("backtrackBeforeAllVisibleElements", function () {
  342. const tallPage = [10, 50];
  343. const shortPage = [10, 10];
  344. const top1 = 20 + SPACING + 40;
  345. const top2 = 20 + SPACING + 10;
  346. it("handles case 1", function () {
  347. 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]]]);
  348. const bsResult = 4;
  349. expect((0, _ui_utils.backtrackBeforeAllVisibleElements)(bsResult, pages, top1)).toEqual(4);
  350. });
  351. it("handles case 2", function () {
  352. 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]]]);
  353. const bsResult = 6;
  354. expect((0, _ui_utils.backtrackBeforeAllVisibleElements)(bsResult, pages, top1)).toEqual(4);
  355. });
  356. it("handles case 3", function () {
  357. 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]]]);
  358. const bsResult = 8;
  359. expect((0, _ui_utils.backtrackBeforeAllVisibleElements)(bsResult, pages, top1)).toEqual(4);
  360. });
  361. it("handles case 4", function () {
  362. 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]]]);
  363. const bsResult = 4;
  364. expect((0, _ui_utils.backtrackBeforeAllVisibleElements)(bsResult, pages, top2)).toEqual(4);
  365. });
  366. });
  367. });
  368. });