Skip to content

Commit 21b6b61

Browse files
authored
test: add unit tests for dispatch-series helper functions (#190)
* test(dispatch-series): add comprehensive unit tests for helper functions * test(dispatch-series): mock setTimeout instead of measuring elapsed time
1 parent 151f36b commit 21b6b61

1 file changed

Lines changed: 194 additions & 0 deletions

File tree

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import {
4+
requireIntInRange,
5+
clampIosSwipeDuration,
6+
shouldUseIosTapSeries,
7+
shouldUseIosDragSeries,
8+
computeDeterministicJitter,
9+
runRepeatedSeries,
10+
} from '../dispatch-series.ts';
11+
import { AppError } from '../../utils/errors.ts';
12+
import type { DeviceInfo } from '../../utils/device.ts';
13+
14+
const iosDevice: DeviceInfo = { platform: 'ios', id: 'test', name: 'iPhone', kind: 'simulator' };
15+
const androidDevice: DeviceInfo = {
16+
platform: 'android',
17+
id: 'test',
18+
name: 'Pixel',
19+
kind: 'emulator',
20+
};
21+
22+
// --- requireIntInRange ---
23+
24+
test('requireIntInRange returns value at lower bound', () => {
25+
assert.equal(requireIntInRange(0, 'x', 0, 10), 0);
26+
});
27+
28+
test('requireIntInRange returns value at upper bound', () => {
29+
assert.equal(requireIntInRange(10, 'x', 0, 10), 10);
30+
});
31+
32+
test('requireIntInRange returns value within range', () => {
33+
assert.equal(requireIntInRange(5, 'x', 0, 10), 5);
34+
});
35+
36+
test('requireIntInRange throws for value below minimum', () => {
37+
assert.throws(
38+
() => requireIntInRange(-1, 'x', 0, 10),
39+
(e: unknown) => e instanceof AppError && e.code === 'INVALID_ARGS',
40+
);
41+
});
42+
43+
test('requireIntInRange throws for value above maximum', () => {
44+
assert.throws(
45+
() => requireIntInRange(11, 'x', 0, 10),
46+
(e: unknown) => e instanceof AppError && e.code === 'INVALID_ARGS',
47+
);
48+
});
49+
50+
test('requireIntInRange throws for non-integer value', () => {
51+
assert.throws(
52+
() => requireIntInRange(5.5, 'x', 0, 10),
53+
(e: unknown) => e instanceof AppError && e.code === 'INVALID_ARGS',
54+
);
55+
});
56+
57+
test('requireIntInRange throws for non-finite values', () => {
58+
for (const value of [NaN, Infinity, -Infinity]) {
59+
assert.throws(
60+
() => requireIntInRange(value, 'x', 0, 10),
61+
(e: unknown) => e instanceof AppError && e.code === 'INVALID_ARGS',
62+
);
63+
}
64+
});
65+
66+
// --- clampIosSwipeDuration ---
67+
68+
test('clampIosSwipeDuration returns value within bounds unchanged', () => {
69+
assert.equal(clampIosSwipeDuration(30), 30);
70+
});
71+
72+
test('clampIosSwipeDuration clamps below-minimum to 16', () => {
73+
assert.equal(clampIosSwipeDuration(5), 16);
74+
});
75+
76+
test('clampIosSwipeDuration clamps above-maximum to 60', () => {
77+
assert.equal(clampIosSwipeDuration(100), 60);
78+
});
79+
80+
test('clampIosSwipeDuration returns exact boundary values unchanged', () => {
81+
assert.equal(clampIosSwipeDuration(16), 16);
82+
assert.equal(clampIosSwipeDuration(60), 60);
83+
});
84+
85+
test('clampIosSwipeDuration rounds fractional input before clamping', () => {
86+
assert.equal(clampIosSwipeDuration(30.4), 30);
87+
assert.equal(clampIosSwipeDuration(15.6), 16);
88+
});
89+
90+
// --- shouldUseIosTapSeries ---
91+
92+
test('shouldUseIosTapSeries returns true for iOS with count > 1 and no hold or jitter', () => {
93+
assert.equal(shouldUseIosTapSeries(iosDevice, 2, 0, 0), true);
94+
});
95+
96+
test('shouldUseIosTapSeries returns false for Android', () => {
97+
assert.equal(shouldUseIosTapSeries(androidDevice, 2, 0, 0), false);
98+
});
99+
100+
test('shouldUseIosTapSeries returns false when count is 1', () => {
101+
assert.equal(shouldUseIosTapSeries(iosDevice, 1, 0, 0), false);
102+
});
103+
104+
test('shouldUseIosTapSeries returns false when holdMs is non-zero', () => {
105+
assert.equal(shouldUseIosTapSeries(iosDevice, 2, 100, 0), false);
106+
});
107+
108+
test('shouldUseIosTapSeries returns false when jitterPx is non-zero', () => {
109+
assert.equal(shouldUseIosTapSeries(iosDevice, 2, 0, 5), false);
110+
});
111+
112+
// --- shouldUseIosDragSeries ---
113+
114+
test('shouldUseIosDragSeries returns true for iOS with count > 1', () => {
115+
assert.equal(shouldUseIosDragSeries(iosDevice, 2), true);
116+
});
117+
118+
test('shouldUseIosDragSeries returns false for Android', () => {
119+
assert.equal(shouldUseIosDragSeries(androidDevice, 2), false);
120+
});
121+
122+
test('shouldUseIosDragSeries returns false when count is 1', () => {
123+
assert.equal(shouldUseIosDragSeries(iosDevice, 1), false);
124+
});
125+
126+
// --- computeDeterministicJitter ---
127+
128+
test('computeDeterministicJitter scales pattern entry by jitter pixels', () => {
129+
assert.deepEqual(computeDeterministicJitter(1, 3), [3, 0]);
130+
});
131+
132+
test('computeDeterministicJitter returns [0, 0] at index 0', () => {
133+
assert.deepEqual(computeDeterministicJitter(0, 5), [0, 0]);
134+
});
135+
136+
test('computeDeterministicJitter cycles through 9-entry pattern', () => {
137+
assert.deepEqual(computeDeterministicJitter(9, 2), [0, 0]);
138+
});
139+
140+
test('computeDeterministicJitter returns [0, 0] when jitterPx is 0', () => {
141+
assert.deepEqual(computeDeterministicJitter(1, 0), [0, 0]);
142+
});
143+
144+
test('computeDeterministicJitter returns [0, 0] when jitterPx is negative', () => {
145+
assert.deepEqual(computeDeterministicJitter(1, -3), [0, 0]);
146+
});
147+
148+
// --- runRepeatedSeries ---
149+
150+
test('runRepeatedSeries invokes operation for each index in order', async () => {
151+
const indices: number[] = [];
152+
await runRepeatedSeries(4, 0, async (i) => {
153+
indices.push(i);
154+
});
155+
assert.deepEqual(indices, [0, 1, 2, 3]);
156+
});
157+
158+
test('runRepeatedSeries does not invoke operation when count is 0', async () => {
159+
const indices: number[] = [];
160+
await runRepeatedSeries(0, 0, async (i) => {
161+
indices.push(i);
162+
});
163+
assert.deepEqual(indices, []);
164+
});
165+
166+
test('runRepeatedSeries pauses between operations but not after the last', async (t) => {
167+
const timeoutDelays: number[] = [];
168+
t.mock.method(globalThis, 'setTimeout', (cb: () => void, ms: number) => {
169+
timeoutDelays.push(ms);
170+
cb();
171+
return 0;
172+
});
173+
const pauseMs = 50;
174+
const calls: number[] = [];
175+
await runRepeatedSeries(3, pauseMs, async (i) => {
176+
calls.push(i);
177+
});
178+
assert.deepEqual(calls, [0, 1, 2]);
179+
// 3 operations with pauses only between them = 2 pauses
180+
assert.deepEqual(timeoutDelays, [pauseMs, pauseMs]);
181+
});
182+
183+
test('runRepeatedSeries propagates operation error and stops iteration', async () => {
184+
const indices: number[] = [];
185+
await assert.rejects(
186+
() =>
187+
runRepeatedSeries(5, 0, async (i) => {
188+
indices.push(i);
189+
if (i === 2) throw new Error('boom');
190+
}),
191+
(e: unknown) => e instanceof Error && e.message === 'boom',
192+
);
193+
assert.deepEqual(indices, [0, 1, 2]);
194+
});

0 commit comments

Comments
 (0)