explodorama / files / test / padDialog.test.ts
padDialog.test.ts
| 1 | import { describe, expect, it } from 'vitest' |
| 2 | import { assignFaces, dismissIndex, faceAction, padMode, restoreIndex, scrollDelta, showsTapHint, startIndex, tabStep, type FooterButton } from '../src/ui/padDialogMap' |
| 3 | |
| 4 | const b = (label: string, kind: FooterButton['kind'] = 'plain'): FooterButton => ({ label, kind }) |
| 5 | |
| 6 | describe('footer buttons on face buttons', () => { |
| 7 | it('primary on Cross, others left to right on Triangle then Square', () => { |
| 8 | expect(assignFaces([b('Keep', 'plain'), b('Take', 'primary'), b('Use', 'primary')], false)).toEqual([3, 0, 2]) |
| 9 | }) |
| 10 | it('the dismissing button takes Circle and leaves the others their faces', () => { |
| 11 | expect(assignFaces([b('Kick it', 'primary'), b('Walk', 'primary'), b('Cancel')], true)).toEqual([0, 3, 1]) |
| 12 | expect(assignFaces([b('Walk there', 'primary'), b('Got it')], true)).toEqual([0, 1]) |
| 13 | }) |
| 14 | it('a required dialog has no Circle, even for a button called Cancel', () => { |
| 15 | expect(assignFaces([b('Cancel')], false)).toEqual([3]) |
| 16 | }) |
| 17 | it('a dialog with no primary leaves Cross unused', () => { |
| 18 | expect(assignFaces([b('Back')], true)).toEqual([1]) |
| 19 | expect(assignFaces([b('A'), b('B')], false)).toEqual([3, 2]) |
| 20 | }) |
| 21 | it('crossTaken keeps Cross free for the body (the shop)', () => { |
| 22 | expect(assignFaces([b('Restock'), b('Next level', 'primary')], false, true)).toEqual([3, 2]) |
| 23 | }) |
| 24 | it('too many buttons: null, so the selector is used', () => { |
| 25 | expect(assignFaces([b('Resume', 'primary'), b('New', 'danger'), b('Almanac'), b('Scores'), b('Demo'), b('Bug')], true)).toBeNull() |
| 26 | expect(assignFaces([b('a', 'primary'), b('b'), b('c'), b('d')], false)).toBeNull() |
| 27 | }) |
| 28 | it('dismissIndex: only when dismissable, by label', () => { |
| 29 | expect(dismissIndex([b('Start'), b('Keep playing')], true)).toBe(1) |
| 30 | expect(dismissIndex([b('Start'), b('Keep playing')], false)).toBe(-1) |
| 31 | expect(dismissIndex([b('Start')], true)).toBe(-1) |
| 32 | }) |
| 33 | }) |
| 34 | |
| 35 | describe('mode and actions', () => { |
| 36 | it('controls in the body force the selector', () => { |
| 37 | expect(padMode([0, 1], 0)).toBe('faces') |
| 38 | expect(padMode([0, 1], 2)).toBe('selector') |
| 39 | expect(padMode(null, 0)).toBe('selector') |
| 40 | }) |
| 41 | it('faces mode: a face presses its footer button; Circle closes when nothing is on it', () => { |
| 42 | const faces = assignFaces([b('Start', 'primary'), b('Other')], false) |
| 43 | expect(faceAction(0, faces, 'faces', false, false)).toEqual({ kind: 'footer', index: 0 }) |
| 44 | expect(faceAction(3, faces, 'faces', false, false)).toEqual({ kind: 'footer', index: 1 }) |
| 45 | expect(faceAction(2, faces, 'faces', false, false)).toBeNull() |
| 46 | expect(faceAction(1, faces, 'faces', false, false)).toBeNull() |
| 47 | expect(faceAction(1, faces, 'faces', true, false)).toEqual({ kind: 'close' }) |
| 48 | }) |
| 49 | it('Nintendo: primary on A (right, 1), the dismissing button on B (bottom, 0), the rest as before', () => { |
| 50 | expect(assignFaces([b('Walk there', 'primary'), b('Got it')], true, false, 'nintendo')).toEqual([1, 0]) |
| 51 | expect(assignFaces([b('Kick it', 'primary'), b('Walk', 'primary'), b('Cancel')], true, false, 'nintendo')).toEqual([1, 3, 0]) |
| 52 | expect(assignFaces([b('Restock'), b('Next level', 'primary')], false, true, 'nintendo')).toEqual([3, 2]) |
| 53 | expect(assignFaces([b('Start', 'primary'), b('Other')], false, false, 'nintendo')).toEqual([1, 3]) |
| 54 | }) |
| 55 | it('Nintendo: A presses the primary / buys / focused, B closes', () => { |
| 56 | const faces = assignFaces([b('Start', 'primary'), b('Other')], false, false, 'nintendo') |
| 57 | expect(faceAction(1, faces, 'faces', false, false, -1, 'nintendo')).toEqual({ kind: 'footer', index: 0 }) |
| 58 | expect(faceAction(0, faces, 'faces', false, false, -1, 'nintendo')).toBeNull() |
| 59 | expect(faceAction(0, faces, 'faces', true, false, -1, 'nintendo')).toEqual({ kind: 'close' }) |
| 60 | expect(faceAction(1, [3, 2], 'faces', false, true, -1, 'nintendo')).toEqual({ kind: 'buy' }) |
| 61 | expect(faceAction(1, null, 'selector', false, false, -1, 'nintendo')).toEqual({ kind: 'focused' }) |
| 62 | expect(faceAction(0, null, 'selector', true, false, -1, 'nintendo')).toEqual({ kind: 'close' }) |
| 63 | expect(faceAction(0, [0], 'selector', true, false, 0, 'nintendo')).toEqual({ kind: 'footer', index: 0 }) |
| 64 | }) |
| 65 | it('Cross buys on a card with a Buy button', () => { |
| 66 | expect(faceAction(0, [3, 2], 'faces', false, true)).toEqual({ kind: 'buy' }) |
| 67 | }) |
| 68 | it('selector mode: Cross presses the focused element, Circle closes, the rest nothing', () => { |
| 69 | expect(faceAction(0, null, 'selector', false, false)).toEqual({ kind: 'focused' }) |
| 70 | expect(faceAction(1, null, 'selector', true, false)).toEqual({ kind: 'close' }) |
| 71 | expect(faceAction(1, null, 'selector', false, false)).toBeNull() |
| 72 | // With a dismissing footer button (Back with an action), Circle presses that button. |
| 73 | expect(faceAction(1, [1], 'selector', true, false, 0)).toEqual({ kind: 'footer', index: 0 }) |
| 74 | expect(faceAction(1, null, 'selector', false, false, 0)).toBeNull() |
| 75 | expect(faceAction(3, null, 'selector', true, false)).toBeNull() |
| 76 | }) |
| 77 | }) |
| 78 | |
| 79 | describe('where the selector starts', () => { |
| 80 | it('the primary footer button, else the pressed item, else the first', () => { |
| 81 | expect(startIndex([{ primary: false, pressed: false }, { primary: false, pressed: true }, { primary: true, pressed: false }])).toBe(2) |
| 82 | expect(startIndex([{ primary: false, pressed: false }, { primary: false, pressed: true }])).toBe(1) |
| 83 | expect(startIndex([{ primary: false, pressed: false }])).toBe(0) |
| 84 | expect(startIndex([])).toBe(0) |
| 85 | }) |
| 86 | }) |
| 87 | |
| 88 | describe('helpers', () => { |
| 89 | it('restoreIndex keeps the place, clamps, and handles empty', () => { |
| 90 | expect(restoreIndex(5, 2)).toBe(2) |
| 91 | expect(restoreIndex(2, 4)).toBe(1) |
| 92 | expect(restoreIndex(3, -1)).toBe(0) |
| 93 | expect(restoreIndex(0, 1)).toBe(-1) |
| 94 | }) |
| 95 | it('tabStep stops at the ends', () => { |
| 96 | expect(tabStep(4, 0, -1)).toBe(0) |
| 97 | expect(tabStep(4, 3, 1)).toBe(3) |
| 98 | expect(tabStep(4, 1, 1)).toBe(2) |
| 99 | }) |
| 100 | it('scrollDelta is proportional to push and time', () => { |
| 101 | expect(scrollDelta(0.5, 0.1)).toBeCloseTo(scrollDelta(1, 0.05)) |
| 102 | expect(scrollDelta(-1, 0.1)).toBeLessThan(0) |
| 103 | }) |
| 104 | }) |
| 105 | |
| 106 | describe('the hint for a confirm button let go too early', () => { |
| 107 | it('shows on a selected item in selector mode, once per dialog, for the confirm button only', () => { |
| 108 | expect(showsTapHint(0, 'selector', true, false, 'xbox')).toBe(true) |
| 109 | expect(showsTapHint(0, 'selector', true, true, 'xbox')).toBe(false) |
| 110 | expect(showsTapHint(0, 'selector', false, false, 'xbox')).toBe(false) |
| 111 | expect(showsTapHint(0, 'faces', true, false, 'xbox')).toBe(false) |
| 112 | expect(showsTapHint(1, 'selector', true, false, 'xbox')).toBe(false) |
| 113 | // A Nintendo pad confirms with its right-hand A (raw 1). |
| 114 | expect(showsTapHint(1, 'selector', true, false, 'nintendo')).toBe(true) |
| 115 | expect(showsTapHint(0, 'selector', true, false, 'nintendo')).toBe(false) |
| 116 | }) |
| 117 | }) |
| 118 | |
| 119 | describe('a Nintendo pad in selector mode', () => { |
| 120 | it('closes with the bottom button (B) and presses the selection with the right one (A)', () => { |
| 121 | // Raw face indices: 0 bottom, 1 right. |
| 122 | expect(faceAction(0, null, 'selector', true, false, -1, 'nintendo')).toEqual({ kind: 'close' }) |
| 123 | expect(faceAction(1, null, 'selector', true, false, -1, 'nintendo')).toEqual({ kind: 'focused' }) |
| 124 | // Elsewhere it is the other way round. |
| 125 | expect(faceAction(1, null, 'selector', true, false, -1, 'xbox')).toEqual({ kind: 'close' }) |
| 126 | expect(faceAction(0, null, 'selector', true, false, -1, 'xbox')).toEqual({ kind: 'focused' }) |
| 127 | }) |
| 128 | }) |