fuzzyworld

explodorama / files / test / padDialog.test.ts

padDialog.test.ts

Blame: the commit that last changed each line · file · history

ebcd0a8f 2026-11-02 Pavel Safronov1import { describe, expect, it } from 'vitest'
92cc4ee3 2026-11-02 Pavel Safronov2import { assignFaces, dismissIndex, faceAction, padMode, restoreIndex, scrollDelta, showsTapHint, startIndex, tabStep, type FooterButton } from '../src/ui/padDialogMap'
ebcd0a8f 2026-11-02 Pavel Safronov3
4const b = (label: string, kind: FooterButton['kind'] = 'plain'): FooterButton => ({ label, kind })
5
6describe('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
35describe('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 })
92cc4ee3 2026-11-02 Pavel Safronov49 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 })
ebcd0a8f 2026-11-02 Pavel Safronov65 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
79describe('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
88describe('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})
92cc4ee3 2026-11-02 Pavel Safronov105
106describe('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
119describe('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})