test/paging.test.ts (5792 bytes)
1 import fc from 'fast-check'; 2 import { describe, expect, it } from 'vitest'; 3 import { 4 arrowsOff, 5 arrowStep, 6 defaultRtl, 7 keyStep, 8 LONG_PRESS_MOVE, 9 movedTooFar, 10 parsePage, 11 SWIPE_MIN, 12 SWIPE_RATIO, 13 swipeStep, 14 type Swipe, 15 } from '../src/paging'; 16 17 const flat = { zoomed: false, wide: false }; 18 19 describe('movedTooFar', () => { 20 it('lets a finger shake up to 10 CSS pixels in a long press', () => { 21 expect(LONG_PRESS_MOVE).toBe(10); 22 expect(movedTooFar(0, 0)).toBe(false); 23 expect(movedTooFar(6, -8)).toBe(false); 24 expect(movedTooFar(7, 8)).toBe(true); 25 }); 26 27 it('never gives a long press for a swipe that turns a page', () => { 28 // A swipe, a tap and a long press never mix. 29 const move = fc.integer({ min: -400, max: 400 }); 30 fc.assert( 31 fc.property(move, move, fc.boolean(), (dx, dy, rtl) => { 32 if (swipeStep({ dx, dy, ...flat }, rtl) !== 0) expect(movedTooFar(dx, dy)).toBe(true); 33 }), 34 ); 35 }); 36 }); 37 38 describe('keyStep', () => { 39 it('turns with the arrow keys and the page keys', () => { 40 expect(keyStep('ArrowRight', false)).toBe(1); 41 expect(keyStep('ArrowLeft', false)).toBe(-1); 42 expect(keyStep('PageDown', false)).toBe(1); 43 expect(keyStep('PageUp', false)).toBe(-1); 44 expect(keyStep('ArrowDown', false)).toBe(0); 45 expect(keyStep(' ', false)).toBe(0); 46 }); 47 48 it('goes forward with ArrowLeft in right-to-left mode', () => { 49 expect(keyStep('ArrowLeft', true)).toBe(1); 50 expect(keyStep('ArrowRight', true)).toBe(-1); 51 expect(keyStep('PageDown', true)).toBe(1); 52 expect(keyStep('PageUp', true)).toBe(-1); 53 }); 54 }); 55 56 describe('swipeStep', () => { 57 it('turns forward for a swipe to the left, back for a swipe to the right', () => { 58 expect(swipeStep({ dx: -80, dy: 10, ...flat }, false)).toBe(1); 59 expect(swipeStep({ dx: 80, dy: -10, ...flat }, false)).toBe(-1); 60 }); 61 62 it('turns forward for a swipe to the right in right-to-left mode', () => { 63 expect(swipeStep({ dx: 80, dy: 0, ...flat }, true)).toBe(1); 64 expect(swipeStep({ dx: -80, dy: 0, ...flat }, true)).toBe(-1); 65 }); 66 67 it('does not turn for a short swipe, a steep swipe, a zoomed page or a wide page', () => { 68 expect(swipeStep({ dx: -49, dy: 0, ...flat }, false)).toBe(0); 69 expect(swipeStep({ dx: -60, dy: 40, ...flat }, false)).toBe(0); 70 expect(swipeStep({ dx: -80, dy: 0, zoomed: true, wide: false }, false)).toBe(0); 71 expect(swipeStep({ dx: -80, dy: 0, zoomed: false, wide: true }, false)).toBe(0); 72 }); 73 74 const swipe: fc.Arbitrary<Swipe> = fc.record({ 75 dx: fc.integer({ min: -400, max: 400 }), 76 dy: fc.integer({ min: -400, max: 400 }), 77 zoomed: fc.boolean(), 78 wide: fc.boolean(), 79 }); 80 81 it('turns exactly for a long, flat swipe on a page that is not zoomed or wide', () => { 82 fc.assert( 83 fc.property(swipe, fc.boolean(), (s, rtl) => { 84 const turns = 85 !s.zoomed && 86 !s.wide && 87 Math.abs(s.dx) >= SWIPE_MIN && 88 Math.abs(s.dx) > SWIPE_RATIO * Math.abs(s.dy); 89 expect(swipeStep(s, rtl) !== 0).toBe(turns); 90 }), 91 ); 92 }); 93 94 it('turns the other way for the mirror swipe, and for the other direction', () => { 95 fc.assert( 96 fc.property(swipe, fc.boolean(), (s, rtl) => { 97 const step = swipeStep(s, rtl); 98 expect(swipeStep({ ...s, dx: -s.dx }, rtl)).toBe(-step || 0); 99 expect(swipeStep(s, !rtl)).toBe(-step || 0); 100 }), 101 ); 102 }); 103 }); 104 105 describe('arrows', () => { 106 it('gives the right arrow the next page, except in right-to-left mode', () => { 107 expect(arrowStep('right', false)).toBe(1); 108 expect(arrowStep('left', false)).toBe(-1); 109 expect(arrowStep('right', true)).toBe(-1); 110 expect(arrowStep('left', true)).toBe(1); 111 }); 112 113 it('turns off the arrow that leads to no page', () => { 114 expect(arrowsOff(0, 40, false)).toEqual({ left: true, right: false }); 115 expect(arrowsOff(39, 40, false)).toEqual({ left: false, right: true }); 116 expect(arrowsOff(0, 40, true)).toEqual({ left: false, right: true }); 117 expect(arrowsOff(0, 1, false)).toEqual({ left: true, right: true }); 118 // No PDF. 119 expect(arrowsOff(-1, 0, false)).toEqual({ left: true, right: true }); 120 }); 121 122 it('turns an arrow off exactly when its step leaves the book', () => { 123 const book = fc 124 .integer({ min: 1, max: 500 }) 125 .chain((pages) => fc.tuple(fc.constant(pages), fc.nat({ max: pages - 1 }), fc.boolean())); 126 fc.assert( 127 fc.property(book, ([pages, page, rtl]) => { 128 const off = arrowsOff(page, pages, rtl); 129 for (const side of ['left', 'right'] as const) { 130 const to = page + arrowStep(side, rtl); 131 expect(off[side]).toBe(to < 0 || to >= pages); 132 } 133 }), 134 ); 135 }); 136 }); 137 138 describe('parsePage', () => { 139 it('reads a page number from 1 to the page count', () => { 140 expect(parsePage('30', 40)).toBe(29); 141 expect(parsePage(' 1 ', 40)).toBe(0); 142 expect(parsePage('30', 40)).toBe(29); 143 }); 144 145 it('gives null for text that is no page of the book', () => { 146 for (const text of ['', '0', '41', '-3', '2.5', '1e1', 'abc', '3 4']) { 147 expect(parsePage(text, 40)).toBeNull(); 148 } 149 }); 150 151 it('reads back each page number that it can show', () => { 152 const book = fc 153 .integer({ min: 1, max: 5000 }) 154 .chain((pages) => fc.tuple(fc.constant(pages), fc.nat({ max: pages - 1 }))); 155 fc.assert( 156 fc.property(book, ([pages, index]) => { 157 expect(parsePage(String(index + 1), pages)).toBe(index); 158 }), 159 ); 160 }); 161 }); 162 163 describe('defaultRtl', () => { 164 it('is right to left for vertical Japanese only', () => { 165 expect(defaultRtl('jpn_vert')).toBe(true); 166 expect(defaultRtl('jpn')).toBe(false); 167 expect(defaultRtl('jpn+eng')).toBe(false); 168 expect(defaultRtl('eng')).toBe(false); 169 }); 170 });