fuzzyworld

explodorama / files / test / keyCursor.test.ts

keyCursor.test.ts

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

92cc4ee3 2026-11-02 Pavel Safronov1import { describe, expect, it } from 'vitest'
720cc9d3 2026-11-03 Pavel Safronov2import { defaultButton, escStep, keyButton, keyDirection, keyRole, modeAfterKey, ignoredForMode, panStep, targetTakesKey, type KeyInfo } from '../src/ui/keyCursor'
92cc4ee3 2026-11-02 Pavel Safronov3import { cursorWanted, tileActions } from '../src/ui/padTiles'
4import { board, v } from './helpers'
5
6const key = (code: string, mods: Partial<KeyInfo> = {}): KeyInfo => ({ code, key: mods.key ?? code, shiftKey: false, altKey: false, ctrlKey: false, metaKey: false, ...mods })
7
8const room = () =>
9 board(`
10 #######
11 #.....#
12 #.....#
13 #..P..#
14 #.....#
15 #.....#
16 #######`)
17
18describe('keyRole', () => {
19 it('Shift + an arrow or W A S D hops; Alt pans; plain is a direction', () => {
20 expect(keyRole(key('ArrowLeft', { shiftKey: true }))).toBe('hop')
21 expect(keyRole(key('KeyW', { shiftKey: true }))).toBe('hop')
22 expect(keyRole(key('ArrowLeft', { altKey: true }))).toBe('pan')
23 expect(keyRole(key('ArrowLeft'))).toBe('direction')
24 expect(keyRole(key('KeyQ'))).toBe('diagonal')
25 expect(keyRole(key('Home'))).toBe('recenter')
26 expect(keyRole(key('Slash', { key: '?', shiftKey: true }))).toBe('inspect')
27 expect(keyRole(key('ShiftLeft', { shiftKey: true }))).toBe('modifier')
28 expect(keyRole(key('KeyM'))).toBe('other')
29 })
30 it('Ctrl and Meta combinations are the browser\'s', () => {
31 expect(keyRole(key('ArrowLeft', { ctrlKey: true }))).toBe('other')
32 expect(keyRole(key('ArrowLeft', { metaKey: true, shiftKey: true }))).toBe('other')
33 })
34 it('Ctrl and Meta with ?, / and Home do nothing for the cursor', () => {
35 for (const mods of [{ ctrlKey: true }, { metaKey: true }, { metaKey: true, shiftKey: true }]) {
36 expect(keyRole(key('Slash', { key: '/', ...mods }))).toBe('other')
37 expect(keyRole(key('Slash', { key: '?', ...mods }))).toBe('other')
38 expect(keyRole(key('Home', mods))).toBe('other')
39 }
40 })
41 it('? is matched by what it types, not by the Slash key (a German layout types - there)', () => {
42 expect(keyRole(key('Slash', { key: '-' }))).toBe('other')
43 expect(keyRole(key('Minus', { key: '?', shiftKey: true }))).toBe('inspect')
44 })
45 it('directions point as the board does (y down)', () => {
46 expect(keyDirection('ArrowUp')).toEqual({ dx: 0, dy: -1 })
47 expect(keyDirection('KeyD')).toEqual({ dx: 1, dy: 0 })
48 expect(keyDirection('KeyM')).toBeNull()
49 })
50})
51
52describe('modeAfterKey', () => {
53 it('while a dialog is open or the demo plays (locked), no key wakes the keyboard, but a pad hands back', () => {
54 expect(modeAfterKey(null, key('ArrowUp', { shiftKey: true }), false, true)).toBeNull()
55 expect(modeAfterKey(null, key('Slash', { key: '?' }), false, true)).toBeNull()
56 expect(modeAfterKey('pad', key('ArrowUp', { shiftKey: true }), false, true)).toBeNull()
57 expect(modeAfterKey('pad', key('KeyM'), false, true)).toBeNull()
58 expect(modeAfterKey('keys', key('KeyM'), false, true)).toBe('keys')
59 })
60 it('Shift + arrow or ? wakes the keyboard, whoever had the board', () => {
61 for (const mode of [null, 'pad', 'keys'] as const) {
62 expect(modeAfterKey(mode, key('ArrowUp', { shiftKey: true }), false)).toBe('keys')
63 expect(modeAfterKey(mode, key('Slash', { key: '?' }), false)).toBe('keys')
64 }
65 })
66 it('the cursor\'s own keys keep the keyboard\'s mode', () => {
67 for (const code of ['ShiftLeft', 'AltLeft', 'Home', 'KeyM', 'KeyB', 'Space', 'Enter', 'Escape', 'Tab']) expect(modeAfterKey('keys', key(code), false)).toBe('keys')
68 expect(modeAfterKey('keys', key('ArrowLeft', { altKey: true }), false)).toBe('keys')
69 })
70 it('a plain arrow or diagonal step hands the board back to the plain keys', () => {
71 expect(modeAfterKey('keys', key('ArrowLeft'), false)).toBeNull()
72 expect(modeAfterKey('keys', key('KeyD'), false)).toBeNull()
73 expect(modeAfterKey('keys', key('KeyQ'), false)).toBeNull()
74 })
75 it('while "What is that?" is on the plain arrows aim, whoever had the board', () => {
76 for (const mode of [null, 'pad', 'keys'] as const) expect(modeAfterKey(mode, key('ArrowLeft'), true)).toBe('keys')
77 })
78 it('a controller\'s board is handed back by every other key, as before', () => {
79 for (const code of ['KeyM', 'ShiftLeft', 'Escape', 'ArrowLeft', 'Home']) expect(modeAfterKey('pad', key(code), false)).toBeNull()
80 })
81 it('with nobody\'s cursor, other keys keep it so', () => {
82 for (const code of ['KeyM', 'Enter', 'ShiftLeft', 'ArrowLeft']) expect(modeAfterKey(null, key(code), false)).toBeNull()
83 })
84})
85
86describe('the cursor shows for either input', () => {
87 const ok = { padActive: true, demo: false, dialogOpen: false, inspecting: false, offerPending: false, autopilot: false, status: 'playing' as const }
88 it('cursorWanted takes "someone has the board" (main.ts passes inputMode !== null)', () => {
89 expect(cursorWanted(ok)).toBe(true)
90 expect(cursorWanted({ ...ok, padActive: false })).toBe(false)
91 })
92})
93
94describe('panStep', () => {
95 it('Up moves the view up (y positive), Left left', () => {
96 expect(panStep('ArrowUp')).toEqual({ x: 0, y: 0.5 })
97 expect(panStep('ArrowLeft')).toEqual({ x: -0.5, y: -0 })
98 expect(panStep('KeyM')).toBeNull()
99 })
100})
101
102describe('targetTakesKey (Enter and Space on a focused control)', () => {
103 it('buttons, links, inputs, selects take it', () => {
104 for (const tagName of ['BUTTON', 'A', 'INPUT', 'TEXTAREA', 'SELECT', 'button']) expect(targetTakesKey({ tagName })).toBe(true)
105 })
106 it('the page, the canvas and a panel do not', () => {
107 for (const tagName of ['BODY', 'CANVAS', 'DIV', 'HTML']) expect(targetTakesKey({ tagName })).toBe(false)
108 expect(targetTakesKey(null)).toBe(false)
109 })
110 it('editable content does', () => {
111 expect(targetTakesKey({ tagName: 'DIV', isContentEditable: true })).toBe(true)
112 })
113})
114
115describe('keyButton', () => {
116 it('M, B, V, Space name their own button when it is possible on the tile', () => {
117 const s = room()
118 const next = tileActions(s, v(4, 3))
119 expect(keyButton(next, 'move')).toBe('move')
120 expect(keyButton(next, 'bomb')).toBe('bomb')
121 expect(keyButton(next, 'wait')).toBeNull()
122 expect(keyButton(tileActions(s, s.player), 'wait')).toBe('wait')
123 })
124 it('a tile that offers nothing for the key gives null (the key then picks the action)', () => {
125 const s = room()
126 expect(keyButton(tileActions(s, v(1, 1)), 'move')).toBeNull()
127 expect(keyButton(tileActions(s, v(4, 3)), 'kick')).toBeNull()
128 })
129})
130
131describe('defaultButton (Enter on the cursor)', () => {
132 it('a tile next to the player: its first action, Move', () => {
133 const s = room()
134 expect(defaultButton(tileActions(s, v(4, 3)), v(4, 3), s.player)).toBe('move')
135 })
136 it('the player\'s own tile: nothing, as a tap there does nothing', () => {
137 const s = room()
138 expect(defaultButton(tileActions(s, s.player), s.player, s.player)).toBeNull()
139 })
140 it('a tile nothing can use: nothing', () => {
141 const s = room()
142 expect(defaultButton(tileActions(s, v(1, 1)), v(1, 1), s.player)).toBeNull()
143 })
144})
145
146describe('escStep', () => {
147 it('cancels what is open first, then brings the cursor home, then puts it away', () => {
148 expect(escStep({ inspecting: true, picked: false, onPlayer: false })).toBe('cancel')
149 expect(escStep({ inspecting: false, picked: true, onPlayer: false })).toBe('cancel')
150 expect(escStep({ inspecting: false, picked: false, onPlayer: false })).toBe('home')
151 expect(escStep({ inspecting: false, picked: false, onPlayer: true })).toBe('hide')
152 })
153})
720cc9d3 2026-11-03 Pavel Safronov154
155describe('ignoredForMode (Q81: a screenshot must not take the board from the pad)', () => {
156 it('a modifier alone and any Cmd combination are ignored; plain play keys are not', () => {
157 expect(ignoredForMode(key('MetaLeft', { key: 'Meta', metaKey: true }))).toBe(true)
158 expect(ignoredForMode(key('ShiftLeft', { key: 'Shift', shiftKey: true }))).toBe(true)
159 expect(ignoredForMode(key('Digit4', { key: '4', metaKey: true, shiftKey: true }))).toBe(true)
160 expect(ignoredForMode(key('ArrowUp', { key: 'ArrowUp', shiftKey: true }))).toBe(false)
161 expect(ignoredForMode(key('KeyM', { key: 'm' }))).toBe(false)
162 })
163})