fuzzyworld

explodorama / files / test / padMap.test.ts

padMap.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 { cancelFace, confirmFace, faceActionOf, faceOf, LEFT_STICK_ENABLED, mapPad as mapPadWith, newPadMemory, padFamily, REPEAT_DELAY_MS, REPEAT_MS, type PadIntent, type PadMemory, type PadSnapshot } from '../src/ui/padMap'
ebcd0a8f 2026-11-02 Pavel Safronov3
4// Tests of the left stick's code run with it on; the default (off) has its own test below.
5const mapPad = (s: PadSnapshot, m: PadMemory, t: number): PadIntent[] => mapPadWith(s, m, t, true)
6
7function snap(pressed: number[] = [], axes: number[] = [0, 0, 0, 0]): PadSnapshot {
8 const buttons = Array.from({ length: 17 }, (_, i) => pressed.includes(i))
9 return { buttons, axes, standard: true }
10}
11// A memory that has already seen its first (waking) frame, as mapPad expects in play.
12function ready(): PadMemory {
13 const mem = newPadMemory()
14 mapPad(snap(), mem, -1000)
15 return mem
16}
17const UP = 12
18const DOWN = 13
19const LEFT = 14
20const RIGHT = 15
21
92cc4ee3 2026-11-02 Pavel Safronov22describe('Nintendo: A (right) is Move/Go, B (bottom) is Wait', () => {
23 const nintendo = (pressed: number[]): PadIntent[] => mapPadWith(snap(pressed), (() => { const m = newPadMemory(); mapPadWith(snap(), m, -1000, true, 'nintendo'); return m })(), 0, true, 'nintendo')
24 it('face 1 (right) is move, face 0 (bottom) is wait; bomb and charge stay left and top', () => {
25 expect(nintendo([1])).toEqual([{ kind: 'press', button: 'move', face: 1 }])
26 expect(nintendo([0])).toEqual([{ kind: 'press', button: 'wait', face: 0 }])
27 expect(nintendo([2])).toEqual([{ kind: 'press', button: 'bomb', face: 2 }])
28 expect(nintendo([3])).toEqual([{ kind: 'press', button: 'charge', face: 3 }])
29 })
30 it('the other buttons are the same on every family', () => {
31 for (const [i, button] of [[4, 'cancel'], [5, 'info'], [8, 'info'], [9, 'menu'], [11, 'reset']] as const) expect(nintendo([i])).toEqual([{ kind: 'press', button }])
32 })
33 it('xbox, playstation and generic keep the position mapping', () => {
34 for (const family of ['xbox', 'playstation', 'generic'] as const) {
35 const m = newPadMemory()
36 mapPadWith(snap(), m, -1000, true, family)
37 expect(mapPadWith(snap([0]), m, 0, true, family)).toEqual([{ kind: 'press', button: 'move', face: 0 }])
38 expect(faceActionOf(family, 1)).toBe('wait')
39 }
40 })
41 it('confirm and cancel faces per family', () => {
42 expect([confirmFace('nintendo'), cancelFace('nintendo')]).toEqual([1, 0])
43 for (const family of ['xbox', 'playstation', 'generic'] as const) expect([confirmFace(family), cancelFace(family)]).toEqual([0, 1])
44 expect([faceOf('nintendo', 'bomb'), faceOf('nintendo', 'charge')]).toEqual([2, 3])
45 })
46})
47
ebcd0a8f 2026-11-02 Pavel Safronov48describe('buttons', () => {
49 const cases: [number, string][] = [[0, 'move'], [1, 'wait'], [2, 'bomb'], [3, 'charge'], [4, 'cancel'], [5, 'info'], [8, 'info'], [9, 'menu'], [11, 'reset']]
50 for (const [index, button] of cases) {
51 it(`button ${index} is ${button}`, () => {
52 expect(mapPad(snap([index]), ready(), 0)).toEqual([index < 4 ? { kind: 'press', button, face: index } : { kind: 'press', button }])
53 })
54 }
55 it('the triggers and L3 do nothing', () => {
56 for (const i of [6, 7, 10]) expect(mapPad(snap([i]), ready(), 0)).toEqual([])
57 })
58 it('fires on the rising edge only', () => {
59 const mem = ready()
60 expect(mapPad(snap([0]), mem, 0)).toHaveLength(1)
61 expect(mapPad(snap([0]), mem, 16)).toEqual([])
62 expect(mapPad(snap([0]), mem, 5000)).toEqual([])
63 expect(mapPad(snap([]), mem, 5016)).toEqual([])
64 expect(mapPad(snap([0]), mem, 5032)).toEqual([{ kind: 'press', button: 'move', face: 0 }])
65 })
66 it('a second button pressed while the first is held fires alone', () => {
67 const mem = ready()
68 mapPad(snap([0]), mem, 0)
69 expect(mapPad(snap([0, 2]), mem, 16)).toEqual([{ kind: 'press', button: 'bomb', face: 2 }])
70 })
71 it('copes with a short buttons array', () => {
72 expect(mapPad({ buttons: [], axes: [], standard: true }, ready(), 0)).toEqual([])
73 })
74})
75
76describe('d-pad direction and repeat', () => {
77 const dirs = (xs: PadIntent[]): PadIntent[] => xs.filter((i) => i.kind === 'dir')
78 it('maps each d-pad button to a direction', () => {
79 expect(mapPad(snap([UP]), ready(), 0)).toEqual([{ kind: 'dir', dx: 0, dy: -1 }])
80 expect(mapPad(snap([DOWN]), ready(), 0)).toEqual([{ kind: 'dir', dx: 0, dy: 1 }])
81 expect(mapPad(snap([LEFT]), ready(), 0)).toEqual([{ kind: 'dir', dx: -1, dy: 0 }])
82 expect(mapPad(snap([RIGHT]), ready(), 0)).toEqual([{ kind: 'dir', dx: 1, dy: 0 }])
83 })
92cc4ee3 2026-11-02 Pavel Safronov84 it('fires at 0, waits REPEAT_DELAY_MS, then repeats every REPEAT_MS', () => {
ebcd0a8f 2026-11-02 Pavel Safronov85 const mem = ready()
86 const fired: number[] = []
92cc4ee3 2026-11-02 Pavel Safronov87 const d = REPEAT_DELAY_MS
88 for (const t of [0, 16, d - 1, d, d + 16, d + REPEAT_MS - 1, d + REPEAT_MS, d + 2 * REPEAT_MS - 1, d + 2 * REPEAT_MS, d + 3 * REPEAT_MS]) {
ebcd0a8f 2026-11-02 Pavel Safronov89 if (dirs(mapPad(snap([RIGHT]), mem, t)).length > 0) fired.push(t)
90 }
92cc4ee3 2026-11-02 Pavel Safronov91 expect(fired).toEqual([0, d, d + REPEAT_MS, d + 2 * REPEAT_MS, d + 3 * REPEAT_MS])
92 })
93 it('holding the d-pad hops quickly (decisions.md 2026-11-02: the cursor crosses the board without a modifier)', () => {
94 expect(REPEAT_DELAY_MS).toBeLessThanOrEqual(250)
95 expect(REPEAT_MS).toBeLessThanOrEqual(90)
96 expect(REPEAT_MS).toBeGreaterThanOrEqual(60)
ebcd0a8f 2026-11-02 Pavel Safronov97 })
98 it('releasing resets, so the next press fires at once', () => {
99 const mem = ready()
100 mapPad(snap([RIGHT]), mem, 0)
101 mapPad(snap([]), mem, 100)
102 expect(dirs(mapPad(snap([RIGHT]), mem, 150))).toHaveLength(1)
92cc4ee3 2026-11-02 Pavel Safronov103 expect(dirs(mapPad(snap([RIGHT]), mem, 150 + REPEAT_DELAY_MS - 1))).toHaveLength(0)
104 expect(dirs(mapPad(snap([RIGHT]), mem, 150 + REPEAT_DELAY_MS))).toHaveLength(1)
ebcd0a8f 2026-11-02 Pavel Safronov105 })
106 it('changing direction while held fires at once and restarts the delay', () => {
107 const mem = ready()
108 mapPad(snap([RIGHT]), mem, 0)
109 expect(mapPad(snap([DOWN]), mem, 100)).toEqual([{ kind: 'dir', dx: 0, dy: 1 }])
92cc4ee3 2026-11-02 Pavel Safronov110 expect(dirs(mapPad(snap([DOWN]), mem, 100 + REPEAT_DELAY_MS - 1))).toHaveLength(0)
111 expect(dirs(mapPad(snap([DOWN]), mem, 100 + REPEAT_DELAY_MS))).toHaveLength(1)
ebcd0a8f 2026-11-02 Pavel Safronov112 })
113 it('gives diagonals; opposite buttons cancel per axis', () => {
114 expect(mapPad(snap([UP, RIGHT]), ready(), 0)).toEqual([{ kind: 'dir', dx: 1, dy: -1 }])
115 expect(mapPad(snap([DOWN, LEFT]), ready(), 0)).toEqual([{ kind: 'dir', dx: -1, dy: 1 }])
116 expect(mapPad(snap([LEFT, RIGHT]), ready(), 0)).toEqual([])
117 expect(mapPad(snap([UP, DOWN]), ready(), 0)).toEqual([])
118 expect(mapPad(snap([UP, LEFT, RIGHT]), ready(), 0)).toEqual([{ kind: 'dir', dx: 0, dy: -1 }])
119 })
92cc4ee3 2026-11-02 Pavel Safronov120 it('repeats on ~16 ms frames: first repeat within a frame of the delay, then about every REPEAT_MS', () => {
ebcd0a8f 2026-11-02 Pavel Safronov121 const mem = ready()
122 const fired: number[] = []
123 for (let t = 0; t <= 800; t += 16) if (dirs(mapPad(snap([RIGHT]), mem, t)).length > 0) fired.push(t)
124 expect(fired[0]).toBe(0)
92cc4ee3 2026-11-02 Pavel Safronov125 expect(fired[1]).toBeGreaterThanOrEqual(REPEAT_DELAY_MS)
126 expect(fired[1]).toBeLessThan(REPEAT_DELAY_MS + 16)
ebcd0a8f 2026-11-02 Pavel Safronov127 for (let i = 2; i < fired.length; i++) {
128 const gap = (fired[i] as number) - (fired[i - 1] as number)
92cc4ee3 2026-11-02 Pavel Safronov129 expect(gap).toBeGreaterThanOrEqual(REPEAT_MS)
130 expect(gap).toBeLessThan(REPEAT_MS + 16)
ebcd0a8f 2026-11-02 Pavel Safronov131 }
132 expect(fired.length).toBeGreaterThanOrEqual(5)
133 })
134 it('going from the d-pad to the stick in the same direction does not restart; a different one does', () => {
135 const mem = ready()
136 mapPad(snap([RIGHT]), mem, 0)
137 expect(mapPad(snap([], [1, 0, 0, 0]), mem, 100)).toEqual([]) // same direction, delay carries on
138 expect(mapPad(snap([], [0, 1, 0, 0]), mem, 150)).toEqual([{ kind: 'dir', dx: 0, dy: 1 }])
92cc4ee3 2026-11-02 Pavel Safronov139 expect(dirs(mapPad(snap([], [0, 1, 0, 0]), mem, 150 + REPEAT_DELAY_MS - 1))).toHaveLength(0)
140 expect(dirs(mapPad(snap([], [0, 1, 0, 0]), mem, 150 + REPEAT_DELAY_MS))).toHaveLength(1)
ebcd0a8f 2026-11-02 Pavel Safronov141 })
142 it('a short buttons array can still carry a press at index 15', () => {
143 const buttons = Array.from({ length: 16 }, (_, i) => i === 15)
144 expect(mapPad({ buttons, axes: [], standard: true }, ready(), 0)).toEqual([{ kind: 'dir', dx: 1, dy: 0 }])
145 })
146})
147
148describe('releasing a diagonal', () => {
149 const dirs = (xs: PadIntent[]): PadIntent[] => xs.filter((i) => i.kind === 'dir')
150 it('one button lifting a frame before the other gives no new step', () => {
151 const mem = ready()
152 expect(mapPad(snap([UP, RIGHT]), mem, 0)).toEqual([{ kind: 'dir', dx: 1, dy: -1 }])
153 expect(mapPad(snap([UP]), mem, 16)).toEqual([])
154 expect(mapPad(snap([]), mem, 32)).toEqual([])
155 // ... and a fresh press afterwards is a step again.
156 expect(mapPad(snap([UP]), mem, 48)).toEqual([{ kind: 'dir', dx: 0, dy: -1 }])
157 })
158 it('the axis left does not repeat either, however long it is held', () => {
159 const mem = ready()
160 mapPad(snap([DOWN, LEFT]), mem, 0)
161 mapPad(snap([LEFT]), mem, 16)
162 expect(dirs(mapPad(snap([LEFT]), mem, 2000))).toHaveLength(0)
163 })
164 it('building a diagonal still fires: the first button, then the second', () => {
165 const mem = ready()
166 expect(mapPad(snap([UP]), mem, 0)).toEqual([{ kind: 'dir', dx: 0, dy: -1 }])
167 expect(mapPad(snap([UP, RIGHT]), mem, 16)).toEqual([{ kind: 'dir', dx: 1, dy: -1 }])
168 })
169 it('going to a different direction, not a part, fires', () => {
170 const mem = ready()
171 mapPad(snap([UP, RIGHT]), mem, 0)
172 expect(mapPad(snap([DOWN, RIGHT]), mem, 16)).toEqual([{ kind: 'dir', dx: 1, dy: 1 }])
173 })
174})
175
176describe('left stick off', () => {
177 it('is off by default and gives nothing, at any push', () => {
178 expect(LEFT_STICK_ENABLED).toBe(false)
179 for (const axes of [[1, 0, 0, 0], [0, -1, 0, 0], [0.7, 0.7, 0, 0]]) expect(mapPadWith(snap([], axes), ready(), 0)).toEqual([])
180 })
181 it('the d-pad still works', () => {
182 expect(mapPadWith(snap([UP], [1, 0, 0, 0]), ready(), 0)).toEqual([{ kind: 'dir', dx: 0, dy: -1 }])
183 })
184})
185
186describe('left stick', () => {
187 const stick = (x: number, y: number): PadSnapshot => snap([], [x, y, 0, 0])
188 it('is ignored below the 0.5 threshold', () => {
189 expect(mapPad(stick(0.49, 0), ready(), 0)).toEqual([])
190 expect(mapPad(stick(0, -0.49), ready(), 0)).toEqual([])
191 })
192 it('gives four straight directions (y positive is down)', () => {
193 expect(mapPad(stick(0.5, 0), ready(), 0)).toEqual([{ kind: 'dir', dx: 1, dy: 0 }])
194 expect(mapPad(stick(-1, 0), ready(), 0)).toEqual([{ kind: 'dir', dx: -1, dy: 0 }])
195 expect(mapPad(stick(0, 0.9), ready(), 0)).toEqual([{ kind: 'dir', dx: 0, dy: 1 }])
196 expect(mapPad(stick(0, -0.9), ready(), 0)).toEqual([{ kind: 'dir', dx: 0, dy: -1 }])
197 })
198 it('gives a diagonal when both axes are past the threshold', () => {
199 expect(mapPad(stick(0.7, 0.7), ready(), 0)).toEqual([{ kind: 'dir', dx: 1, dy: 1 }])
200 expect(mapPad(stick(-0.7, -0.6), ready(), 0)).toEqual([{ kind: 'dir', dx: -1, dy: -1 }])
201 // Sectors are 45 degrees: a push 18 degrees below right is still right; 30 degrees is the diagonal.
202 expect(mapPad(stick(0.9, 0.3), ready(), 0)).toEqual([{ kind: 'dir', dx: 1, dy: 0 }])
203 expect(mapPad(stick(0.8, 0.46), ready(), 0)).toEqual([{ kind: 'dir', dx: 1, dy: 1 }])
204 expect(mapPad(stick(0.3, 0.9), ready(), 0)).toEqual([{ kind: 'dir', dx: 0, dy: 1 }])
205 })
206 it('judges the vector, not each axis: both axes under 0.5 can still be pushed far enough', () => {
207 expect(mapPad(stick(0.45, 0.45), ready(), 0)).toEqual([{ kind: 'dir', dx: 1, dy: 1 }])
208 })
209 it('hysteresis: stays on until it falls to 0.35', () => {
210 const mem = ready()
211 expect(mapPad(stick(0.6, 0), mem, 0)).toHaveLength(1)
212 // 0.4 is under the on-threshold but over the off-threshold: still held, so no new fire.
213 expect(mapPad(stick(0.4, 0), mem, 50)).toEqual([])
214 expect(mem.heldDir).toBe('1,0')
215 expect(mapPad(stick(0.36, 0), mem, 60)).toEqual([])
216 expect(mem.heldDir).toBe('1,0')
217 mapPad(stick(0.35, 0), mem, 70)
218 expect(mem.heldDir).toBe('')
219 // Back at 0.4 it is not on again: it must pass 0.5 first.
220 expect(mapPad(stick(0.4, 0), mem, 80)).toEqual([])
221 expect(mapPad(stick(0.5, 0), mem, 90)).toHaveLength(1)
222 })
223 it('a held stick changes sector only past 30 degrees from the sector centre', () => {
224 const mem = ready()
225 const at = (deg: number): PadSnapshot => stick(0.8 * Math.cos((deg * Math.PI) / 180), 0.8 * Math.sin((deg * Math.PI) / 180))
226 expect(mapPad(at(0), mem, 0)).toEqual([{ kind: 'dir', dx: 1, dy: 0 }])
227 // 29 degrees would be the diagonal from rest, but while held right it stays right.
228 expect(mapPad(at(29), mem, 10)).toEqual([])
229 expect(mem.heldDir).toBe('1,0')
230 expect(mapPad(at(31), mem, 20)).toEqual([{ kind: 'dir', dx: 1, dy: 1 }])
231 // And back: the diagonal holds down to 15 degrees.
232 expect(mapPad(at(20), mem, 30)).toEqual([])
233 expect(mem.heldDir).toBe('1,1')
234 })
235 it('rolling right to down passes through the diagonal, one step each, no flicker', () => {
236 const mem = ready()
237 const steps: string[] = []
238 for (let deg = 0; deg <= 90; deg += 5) {
239 const r = (deg * Math.PI) / 180
240 for (const i of mapPad(stick(0.8 * Math.cos(r), 0.8 * Math.sin(r)), mem, deg)) if (i.kind === 'dir') steps.push(`${i.dx},${i.dy}`)
241 }
242 expect(steps).toEqual(['1,0', '1,1', '0,1'])
243 })
244 it('the opposite side switches sector at once (held right, pushed left)', () => {
245 const mem = ready()
246 mapPad(stick(0.6, 0), mem, 0)
247 expect(mapPad(stick(-0.4, 0), mem, 10)).toEqual([{ kind: 'dir', dx: -1, dy: 0 }])
248 })
249 it('a push to a diagonal and back fires once: the sector the stick is in when it first passes 0.5', () => {
250 // Chosen: the first fire is whatever sector it is in at the first frame past 0.5 (here the
251 // diagonal, 42.7 degrees), and easing back to rest through the band adds nothing.
252 const mem = ready()
253 const fired: PadIntent[] = []
254 const frames: [number, number][] = [[0.3, 0.28], [0.52, 0.48], [0.7, 0.7], [0.5, 0.3], [0.3, 0.1], [0, 0]]
255 frames.forEach(([x, y], i) => fired.push(...mapPad(stick(x, y), mem, i * 16)))
256 expect(fired).toEqual([{ kind: 'dir', dx: 1, dy: 1 }])
257 expect(mem.heldDir).toBe('')
258 })
259 it('non-finite stick values count as zero', () => {
260 expect(mapPad(snap([], [NaN, Infinity, 0, 0]), ready(), 0)).toEqual([])
261 })
262 it('works with only the x axis present', () => {
263 expect(mapPad({ buttons: [], axes: [1], standard: true }, ready(), 0)).toEqual([{ kind: 'dir', dx: 1, dy: 0 }])
264 })
265 it('repeats like the d-pad', () => {
266 const mem = ready()
267 const fired: number[] = []
92cc4ee3 2026-11-02 Pavel Safronov268 const d = REPEAT_DELAY_MS
269 for (const t of [0, d - 1, d, d + REPEAT_MS - 1, d + REPEAT_MS]) if (mapPad(stick(1, 0), mem, t).length > 0) fired.push(t)
270 expect(fired).toEqual([0, d, d + REPEAT_MS])
ebcd0a8f 2026-11-02 Pavel Safronov271 })
272 it('the d-pad takes precedence over the stick', () => {
273 const mem = ready()
274 expect(mapPad(snap([UP], [1, 0, 0, 0]), mem, 0)).toEqual([{ kind: 'dir', dx: 0, dy: -1 }])
275 expect(mapPad(snap([], [1, 0, 0, 0]), mem, 20)).toEqual([{ kind: 'dir', dx: 1, dy: 0 }])
276 })
277 it('copes with missing axes', () => {
278 expect(mapPad({ buttons: [], axes: [], standard: true }, ready(), 0)).toEqual([])
279 })
280})
281
282describe('right stick', () => {
283 const look = (x: number, y: number): PadSnapshot => snap([], [0, 0, x, y])
284 it('does nothing inside the 0.2 deadzone, even before it ever looked', () => {
285 expect(mapPad(look(0.1, 0.1), ready(), 0)).toEqual([])
286 expect(mapPad(look(0.2, 0), ready(), 0)).toEqual([])
287 })
288 it('the deadzone is radial, not per axis', () => {
289 // Each axis is at or under 0.2, but together they are 0.28 out (past the deadzone); 0.14 each is 0.198 (inside).
290 expect(mapPad(look(0.2, 0.2), ready(), 0)).toHaveLength(1)
291 expect(mapPad(look(0.14, 0.14), ready(), 0)).toEqual([])
292 })
293 it('non-finite values count as zero', () => {
294 expect(mapPad(look(NaN, Infinity), ready(), 0)).toEqual([])
295 expect(mapPad(look(NaN, 1), ready(), 0)).toHaveLength(1)
296 })
297 it('scales from 0 at the deadzone edge to 1 at full tilt', () => {
298 const full = mapPad(look(1, 0), ready(), 0)
299 expect(full).toEqual([{ kind: 'look', dx: 1, dy: 0 }])
300 const half = mapPad(look(0.6, 0), ready(), 0)[0]
301 expect(half).toMatchObject({ kind: 'look', dy: 0 })
302 expect((half as { dx: number }).dx).toBeCloseTo(0.5)
303 const down = mapPad(look(0, -1), ready(), 0)[0] as { dx: number; dy: number }
304 expect(down.dx).toBeCloseTo(0)
305 expect(down.dy).toBeCloseTo(-1)
306 })
307 it('stays within -1..1 on a corner push', () => {
308 const l = mapPad(look(1, 1), ready(), 0)[0] as { dx: number; dy: number }
309 expect(Math.abs(l.dx)).toBeLessThanOrEqual(1)
310 expect(Math.abs(l.dy)).toBeLessThanOrEqual(1)
311 expect(l.dx).toBeCloseTo(l.dy)
312 })
313 it('emits look every frame, then one lookEnd on return', () => {
314 const mem: PadMemory = ready()
315 expect(mapPad(look(1, 0), mem, 0)).toHaveLength(1)
316 expect(mapPad(look(1, 0), mem, 16)).toHaveLength(1)
317 expect(mapPad(look(0, 0), mem, 32)).toEqual([{ kind: 'lookEnd' }])
318 expect(mapPad(look(0, 0), mem, 48)).toEqual([])
319 })
320})
321
322describe('the first frame (waking the pad)', () => {
323 it('records what is down but emits no press or dir', () => {
324 const mem = newPadMemory()
325 expect(mapPad(snap([0, RIGHT]), mem, 0)).toEqual([])
326 expect(mem.primed).toBe(true)
327 expect(mapPad(snap([0, RIGHT]), mem, 16)).toEqual([]) // still the same press
328 })
329 it('a direction held through the wake does not repeat, until released', () => {
330 const mem = newPadMemory()
331 mapPad(snap([RIGHT]), mem, 0)
332 expect(mapPad(snap([RIGHT]), mem, 1000)).toEqual([])
333 mapPad(snap([]), mem, 1016)
334 expect(mapPad(snap([RIGHT]), mem, 1032)).toEqual([{ kind: 'dir', dx: 1, dy: 0 }])
335 })
336 it('the next press after the wake fires', () => {
337 const mem = newPadMemory()
338 mapPad(snap([0]), mem, 0)
339 mapPad(snap([]), mem, 16)
340 expect(mapPad(snap([0]), mem, 32)).toEqual([{ kind: 'press', button: 'move', face: 0 }])
341 })
342})
343
344describe('padFamily: Nintendo', () => {
345 it('recognises Switch pads by name or vendor', () => {
346 expect(padFamily('Pro Controller (STANDARD GAMEPAD Vendor: 057e Product: 2009)')).toBe('nintendo')
347 expect(padFamily('Nintendo Switch Pro Controller')).toBe('nintendo')
348 expect(padFamily('057e-2006-Joy-Con (L)')).toBe('nintendo')
349 expect(padFamily('Switch Controller')).toBe('nintendo')
350 })
351})
352
353describe('padFamily', () => {
354 it('recognises Xbox and XInput pads', () => {
355 expect(padFamily('Xbox 360 Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)')).toBe('xbox')
356 expect(padFamily('Microsoft Controller (XInput STANDARD GAMEPAD)')).toBe('xbox')
357 expect(padFamily('XBOX One')).toBe('xbox')
358 })
359 it('recognises PlayStation pads', () => {
360 expect(padFamily('DualSense Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 0ce6)')).toBe('playstation')
361 expect(padFamily('Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 0ce6)')).toBe('playstation')
362 expect(padFamily('054c-09cc-Sony Controller')).toBe('playstation')
363 expect(padFamily('PLAYSTATION(R)3 Controller')).toBe('playstation')
364 expect(padFamily('DualShock 4')).toBe('playstation')
365 })
366 it('anything else is generic', () => {
367 expect(padFamily('8BitDo Pro 2 (Vendor: 2dc8 Product: 6003)')).toBe('generic')
368 expect(padFamily('Wireless Controller')).toBe('generic')
369 expect(padFamily('')).toBe('generic')
370 })
371})