/** * The repeat clock behind `On key down`, checked without a world: the cases that are awkward to * stage in one (a key already held when tracking starts, a hostile interval) live here. */ import { describe, expect, it } from 'vitest'; import { nodeDef } from '../../src/core/graph'; import { MIN_REPEAT_INTERVAL, stepKeyRepeat } from '../../src/core/graph/keyRepeat'; import type { PropertyBag } from '../../src/core/model/Properties'; import { formatNumber } from '../../src/ui/math'; const held = { pressed: false, down: true, dt: 0.1, after: 0.2, interval: 0.2 }; describe('key repeat', () => { it('starts the clock on the press, whatever the frame was worth', () => { const step = stepKeyRepeat(null, { ...held, pressed: true, dt: 5 }); expect(step.fires).toBe(1); expect(step.state).toEqual({ held: 0, repeats: 0 }); }); it('forgets the clock when the key comes up', () => { expect(stepKeyRepeat({ held: 9, repeats: 3 }, { ...held, down: false })).toEqual({ state: null, fires: 0, }); }); it('stays quiet for a key that was already held when it started watching', () => { // No state means the press was never seen – repeating without a first fire would be a lie. expect(stepKeyRepeat(null, held)).toEqual({ state: null, fires: 0 }); }); it('never repeats while the delay is zero, however long the key is held', () => { let state = { held: 0, repeats: 0 }; for (let n = 0; n < 100; n++) { const step = stepKeyRepeat(state, { ...held, after: 0, dt: 1 }); expect(step.fires).toBe(0); state = step.state!; } expect(state.held).toBe(100); }); it('treats a negative delay as no delay at all', () => { expect(stepKeyRepeat({ held: 5, repeats: 0 }, { ...held, after: -1 }).fires).toBe(0); }); it('holds an absurdly small interval to the floor instead of freezing the frame', () => { // Without the floor a 1 ns interval over a 1 s frame would ask for a billion fires. const step = stepKeyRepeat({ held: 0.2, repeats: 0 }, { ...held, dt: 1, interval: 1e-9 }); expect(step.fires).toBeLessThanOrEqual(1 / MIN_REPEAT_INTERVAL + 1); expect(step.fires).toBeGreaterThan(1 / MIN_REPEAT_INTERVAL - 2); }); it('counts every due repeat when one frame swallows several', () => { const step = stepKeyRepeat({ held: 0, repeats: 0 }, { ...held, dt: 1, after: 0.5, interval: 0.5 }); expect(step).toEqual({ state: { held: 1, repeats: 2 }, fires: 2 }); }); }); describe('the repeat rows on the node body', () => { // With an order set, so the order row (#264) stays out of what these look at. const hidden = (params: PropertyBag) => nodeDef('OnKeyDown')?.hiddenParams?.({ order: 1, ...params }); it('leaves both off while the repeat is switched off', () => { expect(hidden({ key: 'a' })).toEqual(['repeatAfter', 'repeatInterval']); expect(hidden({ key: 'a', repeatAfter: 0, repeatInterval: 50 })).toEqual([ 'repeatAfter', 'repeatInterval', ]); }); it('shows both once a delay is set, an interval of zero included', () => { // Zero is a setting here ("every frame"), not an absence – hiding that row would lie. expect(hidden({ key: 'a', repeatAfter: 300, repeatInterval: 0 })).toEqual([]); }); }); describe('the repeat fields in the inspector', () => { const param = (key: string) => { const def = nodeDef('OnKeyDown')?.params.find((p) => p.key === key); return def?.kind === 'number' ? def : null; }; it('types both as whole milliseconds, so the field shows 300 and not 300.000', () => { for (const key of ['repeatAfter', 'repeatInterval']) { expect(param(key)?.decimals).toBe(0); expect(formatNumber(300, param(key)?.decimals)).toBe('300'); } }); });