@flighthq/haptics 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.638.d4dbd4e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/haptics.d.ts +11 -19
- package/dist/haptics.d.ts.map +1 -1
- package/dist/haptics.js +24 -160
- package/dist/haptics.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/haptics.test.ts +130 -330
package/README.md
CHANGED
|
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/haptics`. The `@flig
|
|
|
13
13
|
This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
|
|
14
14
|
|
|
15
15
|
- [Flight project](https://github.com/flighthq/flight)
|
|
16
|
-
- [Source for @flighthq/haptics@0.5.
|
|
17
|
-
- [License](https://github.com/flighthq/flight/blob/
|
|
16
|
+
- [Source for @flighthq/haptics@0.5.1-edge.638.d4dbd4e](https://github.com/flighthq/flight/tree/d4dbd4e347dc8be10b180c85a766ab50333dcb51/packages/haptics)
|
|
17
|
+
- [License](https://github.com/flighthq/flight/blob/d4dbd4e347dc8be10b180c85a766ab50333dcb51/LICENSE.md)
|
package/dist/haptics.d.ts
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export declare function
|
|
4
|
-
export declare function
|
|
5
|
-
export declare function
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function
|
|
8
|
-
export declare function
|
|
9
|
-
export declare function
|
|
10
|
-
export declare function
|
|
11
|
-
export declare function
|
|
12
|
-
export declare function resetHapticsBackendForTest(): void;
|
|
13
|
-
export declare function setHapticsBackend(backend: HapticsBackend | null): void;
|
|
14
|
-
export declare function triggerHapticImpact(style: HapticImpactStyle, intensity?: number): boolean;
|
|
15
|
-
export declare function triggerHapticNotification(type: HapticNotificationType): boolean;
|
|
16
|
-
export declare function triggerHapticSelection(): boolean;
|
|
17
|
-
export declare function vibrateDevice(durationMs: number): boolean;
|
|
18
|
-
export declare function vibrateDevicePattern(pattern: Readonly<number[]>): boolean;
|
|
19
|
-
export declare function vibrateDeviceWaveform(timings: Readonly<number[]>, amplitudes: Readonly<number[]>, repeat?: number): boolean;
|
|
1
|
+
import type { HapticImpactStyle, HapticNotificationType, HapticsCapabilities, HasInputHaptics } from '@flighthq/types/contract';
|
|
2
|
+
export declare function cancelDeviceVibration(host: HasInputHaptics): boolean;
|
|
3
|
+
export declare function getHapticsCapabilities(host: HasInputHaptics, out: HapticsCapabilities): HapticsCapabilities;
|
|
4
|
+
export declare function isHapticsSupported(host: HasInputHaptics): boolean;
|
|
5
|
+
export declare function prepareHaptics(host: HasInputHaptics): void;
|
|
6
|
+
export declare function triggerHapticImpact(host: HasInputHaptics, style: HapticImpactStyle, intensity?: number): boolean;
|
|
7
|
+
export declare function triggerHapticNotification(host: HasInputHaptics, type: HapticNotificationType): boolean;
|
|
8
|
+
export declare function triggerHapticSelection(host: HasInputHaptics): boolean;
|
|
9
|
+
export declare function vibrateDevice(host: HasInputHaptics, durationMs: number): boolean;
|
|
10
|
+
export declare function vibrateDevicePattern(host: HasInputHaptics, pattern: Readonly<number[]>): boolean;
|
|
11
|
+
export declare function vibrateDeviceWaveform(host: HasInputHaptics, timings: Readonly<number[]>, amplitudes: Readonly<number[]>, repeat?: number): boolean;
|
|
20
12
|
//# sourceMappingURL=haptics.d.ts.map
|
package/dist/haptics.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"haptics.d.ts","sourceRoot":"","sources":["../src/haptics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"haptics.d.ts","sourceRoot":"","sources":["../src/haptics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAEpE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,EAAE,mBAAmB,GAAG,mBAAmB,CAE3G;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAEjE;AAID,wBAAgB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAE1D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,iBAAiB,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAEhH;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAEtG;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAErE;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAEhF;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAGhG;AAID,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,EAC3B,UAAU,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,EAC9B,MAAM,SAAK,GACV,OAAO,CAOT"}
|
package/dist/haptics.js
CHANGED
|
@@ -1,123 +1,38 @@
|
|
|
1
|
-
export function cancelDeviceVibration() {
|
|
2
|
-
return
|
|
1
|
+
export function cancelDeviceVibration(host) {
|
|
2
|
+
return host.input.haptics.cancel();
|
|
3
3
|
}
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// coarse approximation of native haptics: it can only buzz the motor for a duration/pattern.
|
|
7
|
-
export function createWebHapticsBackend() {
|
|
8
|
-
return {
|
|
9
|
-
cancel() {
|
|
10
|
-
return _webVibrate(0, 'cancel');
|
|
11
|
-
},
|
|
12
|
-
capabilities(out) {
|
|
13
|
-
const supported = typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function';
|
|
14
|
-
out.amplitudeControl = false;
|
|
15
|
-
out.customEvents = false;
|
|
16
|
-
out.intensity = false;
|
|
17
|
-
out.patterns = supported;
|
|
18
|
-
out.supported = supported;
|
|
19
|
-
return out;
|
|
20
|
-
},
|
|
21
|
-
impact(style, intensity) {
|
|
22
|
-
const base = style === 'heavy' || style === 'rigid' ? 30 : style === 'medium' ? 20 : style === 'soft' ? 25 : 10;
|
|
23
|
-
const ms = intensity !== undefined ? Math.round(base * Math.max(0, Math.min(1, intensity))) : base;
|
|
24
|
-
return _webVibrate(ms, 'impact');
|
|
25
|
-
},
|
|
26
|
-
isSupported() {
|
|
27
|
-
return typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function';
|
|
28
|
-
},
|
|
29
|
-
notification(type) {
|
|
30
|
-
const pattern = type === 'error' ? [20, 60, 20] : type === 'warning' ? [20, 60, 20, 60] : [15, 50, 15];
|
|
31
|
-
return _webVibrate(pattern, 'notification');
|
|
32
|
-
},
|
|
33
|
-
prepare() { },
|
|
34
|
-
selection() {
|
|
35
|
-
return _webVibrate(5, 'selection');
|
|
36
|
-
},
|
|
37
|
-
vibrate(durationMs) {
|
|
38
|
-
return _webVibrate(durationMs, 'vibrate');
|
|
39
|
-
},
|
|
40
|
-
vibratePattern(pattern) {
|
|
41
|
-
if (pattern.length === 0)
|
|
42
|
-
return false;
|
|
43
|
-
return _webVibrate(pattern, 'vibratePattern');
|
|
44
|
-
},
|
|
45
|
-
vibrateWaveform(timings, _amplitudes, repeat) {
|
|
46
|
-
if (timings.length === 0)
|
|
47
|
-
return false;
|
|
48
|
-
void repeat;
|
|
49
|
-
return _webVibrate(timings, 'vibrateWaveform');
|
|
50
|
-
},
|
|
51
|
-
};
|
|
4
|
+
export function getHapticsCapabilities(host, out) {
|
|
5
|
+
return host.input.haptics.capabilities(out);
|
|
52
6
|
}
|
|
53
|
-
export function
|
|
54
|
-
|
|
55
|
-
return { conflict: _hostConflict, layer: 'custom', operation: null, viability: 'unobserved' };
|
|
56
|
-
}
|
|
57
|
-
if (_host !== null) {
|
|
58
|
-
return {
|
|
59
|
-
conflict: _hostConflict,
|
|
60
|
-
layer: 'host',
|
|
61
|
-
operation: _hostObservation !== null ? _hostObservation.operation : null,
|
|
62
|
-
viability: _hostObservation !== null ? _hostObservation.viability : 'unobserved',
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
return { conflict: false, layer: 'host-not-enabled', operation: null, viability: 'unobserved' };
|
|
66
|
-
}
|
|
67
|
-
export function getHapticsBackend() {
|
|
68
|
-
return _custom ?? _host ?? _sentinel;
|
|
69
|
-
}
|
|
70
|
-
export function getHapticsCapabilities(out) {
|
|
71
|
-
return getHapticsBackend().capabilities(out);
|
|
72
|
-
}
|
|
73
|
-
export function installHapticsHostBackend(backend) {
|
|
74
|
-
if (_host !== null) {
|
|
75
|
-
if (_host !== backend)
|
|
76
|
-
_hostConflict = true;
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
_host = backend;
|
|
7
|
+
export function isHapticsSupported(host) {
|
|
8
|
+
return host.input.haptics.isSupported();
|
|
80
9
|
}
|
|
81
|
-
|
|
82
|
-
|
|
10
|
+
// Warm-up hint. `prepare` is optional on the backend, so a host whose provider does not pre-allocate
|
|
11
|
+
// simply does nothing here — that is the absence of a capability, not a failure.
|
|
12
|
+
export function prepareHaptics(host) {
|
|
13
|
+
host.input.haptics.prepare?.();
|
|
83
14
|
}
|
|
84
|
-
export function
|
|
85
|
-
|
|
86
|
-
operation,
|
|
87
|
-
viability: succeeded ? 'available' : 'runtime-api-unavailable',
|
|
88
|
-
};
|
|
15
|
+
export function triggerHapticImpact(host, style, intensity) {
|
|
16
|
+
return host.input.haptics.impact(style, intensity ?? 1);
|
|
89
17
|
}
|
|
90
|
-
export function
|
|
91
|
-
|
|
18
|
+
export function triggerHapticNotification(host, type) {
|
|
19
|
+
return host.input.haptics.notification(type);
|
|
92
20
|
}
|
|
93
|
-
export function
|
|
94
|
-
|
|
95
|
-
_host = null;
|
|
96
|
-
_hostConflict = false;
|
|
97
|
-
_hostObservation = null;
|
|
21
|
+
export function triggerHapticSelection(host) {
|
|
22
|
+
return host.input.haptics.selection();
|
|
98
23
|
}
|
|
99
|
-
export function
|
|
100
|
-
|
|
24
|
+
export function vibrateDevice(host, durationMs) {
|
|
25
|
+
return host.input.haptics.vibrate(durationMs);
|
|
101
26
|
}
|
|
102
|
-
export function
|
|
103
|
-
return getHapticsBackend().impact(style, intensity ?? 1);
|
|
104
|
-
}
|
|
105
|
-
export function triggerHapticNotification(type) {
|
|
106
|
-
return getHapticsBackend().notification(type);
|
|
107
|
-
}
|
|
108
|
-
export function triggerHapticSelection() {
|
|
109
|
-
return getHapticsBackend().selection();
|
|
110
|
-
}
|
|
111
|
-
export function vibrateDevice(durationMs) {
|
|
112
|
-
return getHapticsBackend().vibrate(durationMs);
|
|
113
|
-
}
|
|
114
|
-
export function vibrateDevicePattern(pattern) {
|
|
27
|
+
export function vibrateDevicePattern(host, pattern) {
|
|
115
28
|
if (pattern.length === 0)
|
|
116
29
|
return false;
|
|
117
|
-
return
|
|
30
|
+
return host.input.haptics.vibratePattern(pattern);
|
|
118
31
|
}
|
|
119
|
-
|
|
120
|
-
|
|
32
|
+
// Amplitude-aware waveform, falling back to a plain timing pattern when the selected provider does not
|
|
33
|
+
// implement one. The fallback drops amplitudes rather than failing: every backend can express timings.
|
|
34
|
+
export function vibrateDeviceWaveform(host, timings, amplitudes, repeat = -1) {
|
|
35
|
+
const backend = host.input.haptics;
|
|
121
36
|
if (timings.length === 0)
|
|
122
37
|
return false;
|
|
123
38
|
if (backend.vibrateWaveform !== undefined) {
|
|
@@ -125,55 +40,4 @@ export function vibrateDeviceWaveform(timings, amplitudes, repeat = -1) {
|
|
|
125
40
|
}
|
|
126
41
|
return backend.vibratePattern(timings);
|
|
127
42
|
}
|
|
128
|
-
let _custom = null;
|
|
129
|
-
let _host = null;
|
|
130
|
-
let _hostConflict = false;
|
|
131
|
-
let _hostObservation = null;
|
|
132
|
-
const _sentinel = {
|
|
133
|
-
cancel() {
|
|
134
|
-
return false;
|
|
135
|
-
},
|
|
136
|
-
capabilities(out) {
|
|
137
|
-
out.amplitudeControl = false;
|
|
138
|
-
out.customEvents = false;
|
|
139
|
-
out.intensity = false;
|
|
140
|
-
out.patterns = false;
|
|
141
|
-
out.supported = false;
|
|
142
|
-
return out;
|
|
143
|
-
},
|
|
144
|
-
impact() {
|
|
145
|
-
return false;
|
|
146
|
-
},
|
|
147
|
-
isSupported() {
|
|
148
|
-
return false;
|
|
149
|
-
},
|
|
150
|
-
notification() {
|
|
151
|
-
return false;
|
|
152
|
-
},
|
|
153
|
-
prepare() { },
|
|
154
|
-
selection() {
|
|
155
|
-
return false;
|
|
156
|
-
},
|
|
157
|
-
vibrate() {
|
|
158
|
-
return false;
|
|
159
|
-
},
|
|
160
|
-
vibratePattern() {
|
|
161
|
-
return false;
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
function _webVibrate(pattern, operation) {
|
|
165
|
-
if (typeof navigator === 'undefined' || !('vibrate' in navigator) || typeof navigator.vibrate !== 'function') {
|
|
166
|
-
observeHapticsHostResult(operation, false);
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
try {
|
|
170
|
-
const result = navigator.vibrate(pattern);
|
|
171
|
-
observeHapticsHostResult(operation, result);
|
|
172
|
-
return result;
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
observeHapticsHostResult(operation, false);
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
43
|
//# sourceMappingURL=haptics.js.map
|
package/dist/haptics.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"haptics.js","sourceRoot":"","sources":["../src/haptics.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"haptics.js","sourceRoot":"","sources":["../src/haptics.ts"],"names":[],"mappings":"AAOA,MAAM,UAAU,qBAAqB,CAAC,IAAqB;IACzD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,IAAqB,EAAE,GAAwB;IACpF,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAqB;IACtD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;AAC1C,CAAC;AAED,qGAAqG;AACrG,iFAAiF;AACjF,MAAM,UAAU,cAAc,CAAC,IAAqB;IAClD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,IAAqB,EAAE,KAAwB,EAAE,SAAkB;IACrG,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS,IAAI,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,IAAqB,EAAE,IAA4B;IAC3F,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,IAAqB;IAC1D,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAqB,EAAE,UAAkB;IACrE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,IAAqB,EAAE,OAA2B;IACrF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;AACpD,CAAC;AAED,uGAAuG;AACvG,uGAAuG;AACvG,MAAM,UAAU,qBAAqB,CACnC,IAAqB,EACrB,OAA2B,EAC3B,UAA8B,EAC9B,MAAM,GAAG,CAAC,CAAC;IAEX,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;IACnC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,IAAI,OAAO,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;QAC1C,OAAO,OAAO,CAAC,eAAe,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;AACzC,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { cancelDeviceVibration, getHapticsCapabilities, isHapticsSupported, prepareHaptics, triggerHapticImpact, triggerHapticNotification, triggerHapticSelection, vibrateDevice, vibrateDevicePattern, vibrateDeviceWaveform, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,yBAAyB,EACzB,sBAAsB,EACtB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { cancelDeviceVibration, getHapticsCapabilities, isHapticsSupported, prepareHaptics, triggerHapticImpact, triggerHapticNotification, triggerHapticSelection, vibrateDevice, vibrateDevicePattern, vibrateDeviceWaveform, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,yBAAyB,EACzB,sBAAsB,EACtB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/haptics",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1-edge.638.d4dbd4e",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/types": "0.5.
|
|
41
|
+
"@flighthq/types": "0.5.1-edge.638.d4dbd4e"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"typescript": "^5.3.0"
|
package/src/haptics.test.ts
CHANGED
|
@@ -3,28 +3,27 @@ import type {
|
|
|
3
3
|
HapticNotificationType,
|
|
4
4
|
HapticsBackend,
|
|
5
5
|
HapticsCapabilities,
|
|
6
|
+
HasInputHaptics,
|
|
6
7
|
} from '@flighthq/types/contract';
|
|
7
8
|
|
|
8
9
|
import {
|
|
9
10
|
cancelDeviceVibration,
|
|
10
|
-
createWebHapticsBackend,
|
|
11
|
-
getHapticsBackend,
|
|
12
11
|
getHapticsCapabilities,
|
|
13
12
|
isHapticsSupported,
|
|
14
13
|
prepareHaptics,
|
|
15
|
-
setHapticsBackend,
|
|
16
14
|
triggerHapticImpact,
|
|
17
15
|
triggerHapticNotification,
|
|
18
16
|
triggerHapticSelection,
|
|
19
17
|
vibrateDevice,
|
|
20
18
|
vibrateDevicePattern,
|
|
21
19
|
vibrateDeviceWaveform,
|
|
22
|
-
explainHapticsBackend,
|
|
23
|
-
installHapticsHostBackend,
|
|
24
|
-
observeHapticsHostResult,
|
|
25
|
-
resetHapticsBackendForTest,
|
|
26
20
|
} from './haptics';
|
|
27
21
|
|
|
22
|
+
interface RecordedCall {
|
|
23
|
+
readonly args: readonly unknown[];
|
|
24
|
+
readonly name: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
28
27
|
function makeCapabilities(overrides: Partial<HapticsCapabilities> = {}): HapticsCapabilities {
|
|
29
28
|
return {
|
|
30
29
|
amplitudeControl: false,
|
|
@@ -36,391 +35,192 @@ function makeCapabilities(overrides: Partial<HapticsCapabilities> = {}): Haptics
|
|
|
36
35
|
};
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
duration: -1,
|
|
53
|
-
lastPattern: null,
|
|
54
|
-
lastWaveform: null,
|
|
55
|
-
prepared: 0,
|
|
56
|
-
style: null,
|
|
57
|
-
styleIntensity: undefined,
|
|
58
|
-
type: null,
|
|
59
|
-
selections: 0,
|
|
60
|
-
cancel() {
|
|
61
|
-
this.cancelCount += 1;
|
|
62
|
-
return true;
|
|
63
|
-
},
|
|
38
|
+
// A host carrying a recording haptics provider. Every operation is explicit here — there is no ambient
|
|
39
|
+
// slot to install into and nothing to reset between tests, which is the point of the migration: two
|
|
40
|
+
// tests can hold two different providers at once without interfering.
|
|
41
|
+
function makeHost(overrides: Partial<HapticsBackend> = {}): { calls: RecordedCall[]; host: HasInputHaptics } {
|
|
42
|
+
const calls: RecordedCall[] = [];
|
|
43
|
+
const record =
|
|
44
|
+
(name: string, result: boolean) =>
|
|
45
|
+
(...args: unknown[]): boolean => {
|
|
46
|
+
calls.push({ args, name });
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
const haptics: HapticsBackend = {
|
|
50
|
+
cancel: record('cancel', true),
|
|
64
51
|
capabilities(out: HapticsCapabilities): HapticsCapabilities {
|
|
65
|
-
|
|
66
|
-
out.customEvents = false;
|
|
67
|
-
out.intensity = true;
|
|
52
|
+
calls.push({ args: [], name: 'capabilities' });
|
|
68
53
|
out.patterns = true;
|
|
69
54
|
out.supported = true;
|
|
70
55
|
return out;
|
|
71
56
|
},
|
|
72
|
-
impact
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
},
|
|
80
|
-
notification(type: HapticNotificationType) {
|
|
81
|
-
this.type = type;
|
|
82
|
-
return true;
|
|
83
|
-
},
|
|
84
|
-
prepare() {
|
|
85
|
-
this.prepared += 1;
|
|
86
|
-
},
|
|
87
|
-
selection() {
|
|
88
|
-
this.selections += 1;
|
|
89
|
-
return true;
|
|
90
|
-
},
|
|
91
|
-
vibrate(durationMs: number) {
|
|
92
|
-
this.duration = durationMs;
|
|
93
|
-
return true;
|
|
94
|
-
},
|
|
95
|
-
vibratePattern(pattern: Readonly<number[]>) {
|
|
96
|
-
this.lastPattern = pattern;
|
|
97
|
-
return true;
|
|
98
|
-
},
|
|
99
|
-
vibrateWaveform(timings: Readonly<number[]>, amplitudes: Readonly<number[]>, repeat?: number) {
|
|
100
|
-
this.lastWaveform = { amplitudes, repeat, timings };
|
|
101
|
-
return true;
|
|
102
|
-
},
|
|
57
|
+
impact: record('impact', true) as HapticsBackend['impact'],
|
|
58
|
+
isSupported: record('isSupported', true) as HapticsBackend['isSupported'],
|
|
59
|
+
notification: record('notification', true) as HapticsBackend['notification'],
|
|
60
|
+
selection: record('selection', true) as HapticsBackend['selection'],
|
|
61
|
+
vibrate: record('vibrate', true) as HapticsBackend['vibrate'],
|
|
62
|
+
vibratePattern: record('vibratePattern', true) as HapticsBackend['vibratePattern'],
|
|
63
|
+
...overrides,
|
|
103
64
|
};
|
|
65
|
+
return { calls, host: { input: { haptics } } };
|
|
104
66
|
}
|
|
105
67
|
|
|
106
|
-
afterEach(() => setHapticsBackend(null));
|
|
107
|
-
|
|
108
68
|
describe('cancelDeviceVibration', () => {
|
|
109
|
-
it('forwards to
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
expect(
|
|
113
|
-
expect(backend.cancelCount).toBe(1);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('returns false when haptics unavailable (jsdom)', () => {
|
|
117
|
-
expect(cancelDeviceVibration()).toBe(false);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe('createWebHapticsBackend', () => {
|
|
122
|
-
it('returns false for every method without throwing when vibrate is unavailable (jsdom)', () => {
|
|
123
|
-
const backend = createWebHapticsBackend();
|
|
124
|
-
expect(backend.vibrate(10)).toBe(false);
|
|
125
|
-
expect(backend.impact('heavy')).toBe(false);
|
|
126
|
-
expect(backend.impact('rigid')).toBe(false);
|
|
127
|
-
expect(backend.impact('soft')).toBe(false);
|
|
128
|
-
expect(backend.notification('error')).toBe(false);
|
|
129
|
-
expect(backend.selection()).toBe(false);
|
|
130
|
-
expect(backend.vibratePattern([100, 50, 100])).toBe(false);
|
|
131
|
-
expect(backend.cancel()).toBe(false);
|
|
132
|
-
expect(backend.isSupported()).toBe(false);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('returns false for empty pattern without throwing', () => {
|
|
136
|
-
const backend = createWebHapticsBackend();
|
|
137
|
-
expect(backend.vibratePattern([])).toBe(false);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('returns false for waveform with empty timings', () => {
|
|
141
|
-
const backend = createWebHapticsBackend();
|
|
142
|
-
expect(backend.vibrateWaveform!([], [], undefined)).toBe(false);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('fills capabilities for web (no vibrate)', () => {
|
|
146
|
-
const backend = createWebHapticsBackend();
|
|
147
|
-
const out = makeCapabilities();
|
|
148
|
-
backend.capabilities(out);
|
|
149
|
-
expect(out.supported).toBe(false);
|
|
150
|
-
expect(out.patterns).toBe(false);
|
|
151
|
-
expect(out.intensity).toBe(false);
|
|
152
|
-
expect(out.amplitudeControl).toBe(false);
|
|
153
|
-
expect(out.customEvents).toBe(false);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('prepare is a no-op and does not throw', () => {
|
|
157
|
-
const backend = createWebHapticsBackend();
|
|
158
|
-
expect(() => backend.prepare!()).not.toThrow();
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe('explainHapticsBackend', () => {
|
|
163
|
-
afterEach(() => resetHapticsBackendForTest());
|
|
164
|
-
|
|
165
|
-
it('reports host-not-enabled when no backend is installed', () => {
|
|
166
|
-
resetHapticsBackendForTest();
|
|
167
|
-
const explanation = explainHapticsBackend();
|
|
168
|
-
expect(explanation.layer).toBe('host-not-enabled');
|
|
169
|
-
expect(explanation.conflict).toBe(false);
|
|
170
|
-
expect(explanation.viability).toBe('unobserved');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('reports custom layer when a custom backend is set', () => {
|
|
174
|
-
setHapticsBackend(fakeBackend());
|
|
175
|
-
expect(explainHapticsBackend().layer).toBe('custom');
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('reports host layer when a host backend is installed', () => {
|
|
179
|
-
installHapticsHostBackend(fakeBackend());
|
|
180
|
-
expect(explainHapticsBackend().layer).toBe('host');
|
|
69
|
+
it('forwards to the selected provider', () => {
|
|
70
|
+
const { calls, host } = makeHost();
|
|
71
|
+
expect(cancelDeviceVibration(host)).toBe(true);
|
|
72
|
+
expect(calls.map((call) => call.name)).toEqual(['cancel']);
|
|
181
73
|
});
|
|
182
74
|
|
|
183
|
-
it('
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
expect(explainHapticsBackend().conflict).toBe(true);
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe('getHapticsBackend', () => {
|
|
191
|
-
it('falls back to a web backend', () => {
|
|
192
|
-
expect(getHapticsBackend()).not.toBeNull();
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('returns the registered backend', () => {
|
|
196
|
-
const backend = fakeBackend();
|
|
197
|
-
setHapticsBackend(backend);
|
|
198
|
-
expect(getHapticsBackend()).toBe(backend);
|
|
75
|
+
it('returns whatever the provider reports rather than assuming success', () => {
|
|
76
|
+
const { host } = makeHost({ cancel: () => false });
|
|
77
|
+
expect(cancelDeviceVibration(host)).toBe(false);
|
|
199
78
|
});
|
|
200
79
|
});
|
|
201
80
|
|
|
202
81
|
describe('getHapticsCapabilities', () => {
|
|
203
|
-
it('fills
|
|
204
|
-
const
|
|
205
|
-
setHapticsBackend(backend);
|
|
82
|
+
it('fills and returns the caller-owned out parameter', () => {
|
|
83
|
+
const { host } = makeHost();
|
|
206
84
|
const out = makeCapabilities();
|
|
207
|
-
|
|
208
|
-
expect(result).toBe(out);
|
|
85
|
+
expect(getHapticsCapabilities(host, out)).toBe(out);
|
|
209
86
|
expect(out.supported).toBe(true);
|
|
210
|
-
expect(out.intensity).toBe(true);
|
|
211
87
|
expect(out.patterns).toBe(true);
|
|
212
|
-
expect(out.amplitudeControl).toBe(true);
|
|
213
|
-
expect(out.customEvents).toBe(false);
|
|
214
88
|
});
|
|
215
89
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
90
|
+
// ★ Two hosts, two providers, no interference. Under the ambient model the second install either
|
|
91
|
+
// overwrote the first or was refused as a conflict; there was exactly one answer per process.
|
|
92
|
+
it('reports each host independently when two providers are live at once', () => {
|
|
93
|
+
const supported = makeHost();
|
|
94
|
+
const unsupported = makeHost({
|
|
95
|
+
capabilities: (out: HapticsCapabilities): HapticsCapabilities => {
|
|
96
|
+
out.supported = false;
|
|
97
|
+
out.patterns = false;
|
|
98
|
+
return out;
|
|
99
|
+
},
|
|
100
|
+
});
|
|
225
101
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
installHapticsHostBackend(backend);
|
|
229
|
-
expect(getHapticsBackend()).toBe(backend);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('is first-host-wins: a second different backend sets conflict', () => {
|
|
233
|
-
const first = fakeBackend();
|
|
234
|
-
const second = fakeBackend();
|
|
235
|
-
installHapticsHostBackend(first);
|
|
236
|
-
installHapticsHostBackend(second);
|
|
237
|
-
expect(getHapticsBackend()).toBe(first);
|
|
238
|
-
expect(explainHapticsBackend().conflict).toBe(true);
|
|
102
|
+
expect(getHapticsCapabilities(supported.host, makeCapabilities()).supported).toBe(true);
|
|
103
|
+
expect(getHapticsCapabilities(unsupported.host, makeCapabilities()).supported).toBe(false);
|
|
239
104
|
});
|
|
240
105
|
});
|
|
241
106
|
|
|
242
107
|
describe('isHapticsSupported', () => {
|
|
243
|
-
it('
|
|
244
|
-
|
|
245
|
-
expect(isHapticsSupported()).toBe(true);
|
|
108
|
+
it('forwards to the selected provider', () => {
|
|
109
|
+
const { host } = makeHost();
|
|
110
|
+
expect(isHapticsSupported(host)).toBe(true);
|
|
246
111
|
});
|
|
247
112
|
|
|
248
|
-
it('
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
describe('observeHapticsHostResult', () => {
|
|
254
|
-
afterEach(() => resetHapticsBackendForTest());
|
|
255
|
-
|
|
256
|
-
it('records a successful observation', () => {
|
|
257
|
-
installHapticsHostBackend(fakeBackend());
|
|
258
|
-
observeHapticsHostResult('cancel', true);
|
|
259
|
-
const explanation = explainHapticsBackend();
|
|
260
|
-
expect(explanation.operation).toBe('cancel');
|
|
261
|
-
expect(explanation.viability).toBe('available');
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('records a failed observation', () => {
|
|
265
|
-
installHapticsHostBackend(fakeBackend());
|
|
266
|
-
observeHapticsHostResult('cancel', false);
|
|
267
|
-
expect(explainHapticsBackend().viability).toBe('runtime-api-unavailable');
|
|
113
|
+
it('reports false for a provider that says so', () => {
|
|
114
|
+
const { host } = makeHost({ isSupported: () => false });
|
|
115
|
+
expect(isHapticsSupported(host)).toBe(false);
|
|
268
116
|
});
|
|
269
117
|
});
|
|
270
118
|
|
|
271
119
|
describe('prepareHaptics', () => {
|
|
272
|
-
it('calls prepare on
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
120
|
+
it('calls prepare on providers that implement it', () => {
|
|
121
|
+
let prepared = false;
|
|
122
|
+
const { host } = makeHost({
|
|
123
|
+
prepare: (): void => {
|
|
124
|
+
prepared = true;
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
prepareHaptics(host);
|
|
128
|
+
expect(prepared).toBe(true);
|
|
277
129
|
});
|
|
278
130
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
expect(() => prepareHaptics()).not.toThrow();
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
describe('resetHapticsBackendForTest', () => {
|
|
289
|
-
it('clears all backend slots', () => {
|
|
290
|
-
setHapticsBackend(fakeBackend());
|
|
291
|
-
installHapticsHostBackend(fakeBackend());
|
|
292
|
-
observeHapticsHostResult('cancel', true);
|
|
293
|
-
resetHapticsBackendForTest();
|
|
294
|
-
expect(explainHapticsBackend().layer).toBe('host-not-enabled');
|
|
295
|
-
expect(explainHapticsBackend().conflict).toBe(false);
|
|
296
|
-
expect(explainHapticsBackend().viability).toBe('unobserved');
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
describe('setHapticsBackend', () => {
|
|
301
|
-
it('clears back to the web fallback when passed null', () => {
|
|
302
|
-
setHapticsBackend(fakeBackend());
|
|
303
|
-
setHapticsBackend(null);
|
|
304
|
-
expect(getHapticsBackend()).not.toBeNull();
|
|
131
|
+
// prepare is optional on HapticsBackend. A provider that does not pre-allocate is not a broken
|
|
132
|
+
// provider, so the absence has to be silent rather than a throw.
|
|
133
|
+
it('does nothing when the provider omits prepare', () => {
|
|
134
|
+
const { host } = makeHost();
|
|
135
|
+
expect(host.input.haptics.prepare).toBeUndefined();
|
|
136
|
+
expect(() => prepareHaptics(host)).not.toThrow();
|
|
305
137
|
});
|
|
306
138
|
});
|
|
307
139
|
|
|
308
140
|
describe('triggerHapticImpact', () => {
|
|
309
|
-
it('forwards
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
expect(
|
|
313
|
-
expect(backend.style).toBe('medium');
|
|
314
|
-
expect(backend.styleIntensity).toBe(1);
|
|
141
|
+
it('forwards the style and defaults intensity to 1', () => {
|
|
142
|
+
const { calls, host } = makeHost();
|
|
143
|
+
triggerHapticImpact(host, 'heavy');
|
|
144
|
+
expect(calls[0]).toEqual({ args: ['heavy', 1], name: 'impact' });
|
|
315
145
|
});
|
|
316
146
|
|
|
317
|
-
it('forwards
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
expect(backend.style).toBe('heavy');
|
|
322
|
-
expect(backend.styleIntensity).toBe(0.5);
|
|
147
|
+
it('forwards an explicit intensity unchanged', () => {
|
|
148
|
+
const { calls, host } = makeHost();
|
|
149
|
+
triggerHapticImpact(host, 'light', 0.25);
|
|
150
|
+
expect(calls[0]).toEqual({ args: ['light', 0.25], name: 'impact' });
|
|
323
151
|
});
|
|
324
152
|
|
|
325
|
-
it('
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
triggerHapticImpact(
|
|
329
|
-
expect(
|
|
330
|
-
triggerHapticImpact('soft');
|
|
331
|
-
expect(backend.style).toBe('soft');
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it('web backend clamps intensity to 0..1', () => {
|
|
335
|
-
const webBackend = createWebHapticsBackend();
|
|
336
|
-
// Should not throw on out-of-range intensity even if vibrate returns false
|
|
337
|
-
expect(() => webBackend.impact('heavy', 2)).not.toThrow();
|
|
338
|
-
expect(() => webBackend.impact('heavy', -0.5)).not.toThrow();
|
|
153
|
+
it('forwards every impact style', () => {
|
|
154
|
+
const styles: readonly HapticImpactStyle[] = ['heavy', 'light', 'medium', 'rigid', 'soft'];
|
|
155
|
+
const { calls, host } = makeHost();
|
|
156
|
+
for (const style of styles) triggerHapticImpact(host, style);
|
|
157
|
+
expect(calls.map((call) => call.args[0])).toEqual(styles);
|
|
339
158
|
});
|
|
340
159
|
});
|
|
341
160
|
|
|
342
161
|
describe('triggerHapticNotification', () => {
|
|
343
|
-
it('forwards
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
expect(
|
|
162
|
+
it('forwards every notification type', () => {
|
|
163
|
+
const types: readonly HapticNotificationType[] = ['error', 'success', 'warning'];
|
|
164
|
+
const { calls, host } = makeHost();
|
|
165
|
+
for (const type of types) triggerHapticNotification(host, type);
|
|
166
|
+
expect(calls.map((call) => call.args[0])).toEqual(types);
|
|
348
167
|
});
|
|
349
168
|
});
|
|
350
169
|
|
|
351
170
|
describe('triggerHapticSelection', () => {
|
|
352
|
-
it('
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
expect(
|
|
356
|
-
expect(backend.selections).toBe(1);
|
|
171
|
+
it('forwards to the selected provider', () => {
|
|
172
|
+
const { calls, host } = makeHost();
|
|
173
|
+
expect(triggerHapticSelection(host)).toBe(true);
|
|
174
|
+
expect(calls.map((call) => call.name)).toEqual(['selection']);
|
|
357
175
|
});
|
|
358
176
|
});
|
|
359
177
|
|
|
360
178
|
describe('vibrateDevice', () => {
|
|
361
|
-
it('forwards duration to the
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
expect(
|
|
365
|
-
expect(backend.duration).toBe(42);
|
|
179
|
+
it('forwards the duration to the selected provider', () => {
|
|
180
|
+
const { calls, host } = makeHost();
|
|
181
|
+
vibrateDevice(host, 42);
|
|
182
|
+
expect(calls[0]).toEqual({ args: [42], name: 'vibrate' });
|
|
366
183
|
});
|
|
367
184
|
});
|
|
368
185
|
|
|
369
186
|
describe('vibrateDevicePattern', () => {
|
|
370
|
-
it('forwards
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
expect(vibrateDevicePattern(pattern)).toBe(true);
|
|
375
|
-
expect(backend.lastPattern).toEqual(pattern);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it('returns false for empty pattern', () => {
|
|
379
|
-
const backend = fakeBackend();
|
|
380
|
-
setHapticsBackend(backend);
|
|
381
|
-
expect(vibrateDevicePattern([])).toBe(false);
|
|
187
|
+
it('forwards a non-empty pattern', () => {
|
|
188
|
+
const { calls, host } = makeHost();
|
|
189
|
+
vibrateDevicePattern(host, [10, 20]);
|
|
190
|
+
expect(calls[0]).toEqual({ args: [[10, 20]], name: 'vibratePattern' });
|
|
382
191
|
});
|
|
383
192
|
|
|
384
|
-
it('
|
|
385
|
-
|
|
193
|
+
it('rejects an empty pattern before reaching the provider', () => {
|
|
194
|
+
const { calls, host } = makeHost();
|
|
195
|
+
expect(vibrateDevicePattern(host, [])).toBe(false);
|
|
196
|
+
expect(calls).toEqual([]);
|
|
386
197
|
});
|
|
387
198
|
});
|
|
388
199
|
|
|
389
200
|
describe('vibrateDeviceWaveform', () => {
|
|
390
|
-
it('
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
expect(vibrateDeviceWaveform(
|
|
413
|
-
expect(
|
|
414
|
-
expect(backend.lastWaveform).toBeNull();
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
it('returns false for empty timings', () => {
|
|
418
|
-
const backend = fakeBackend();
|
|
419
|
-
setHapticsBackend(backend);
|
|
420
|
-
expect(vibrateDeviceWaveform([], [], undefined)).toBe(false);
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
it('returns false when haptics unavailable (jsdom)', () => {
|
|
424
|
-
expect(vibrateDeviceWaveform([100], [255])).toBe(false);
|
|
201
|
+
it('uses vibrateWaveform when the provider implements it', () => {
|
|
202
|
+
const { calls, host } = makeHost({
|
|
203
|
+
vibrateWaveform: (...args: unknown[]): boolean => {
|
|
204
|
+
calls.push({ args, name: 'vibrateWaveform' });
|
|
205
|
+
return true;
|
|
206
|
+
},
|
|
207
|
+
} as Partial<HapticsBackend>);
|
|
208
|
+
expect(vibrateDeviceWaveform(host, [10, 20], [255, 128], 1)).toBe(true);
|
|
209
|
+
expect(calls[0]).toEqual({ args: [[10, 20], [255, 128], 1], name: 'vibrateWaveform' });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// The fallback drops amplitudes rather than failing. Every provider can express timings, so a
|
|
213
|
+
// provider without amplitude support should still buzz rather than silently do nothing.
|
|
214
|
+
it('falls back to vibratePattern with the timings when vibrateWaveform is absent', () => {
|
|
215
|
+
const { calls, host } = makeHost();
|
|
216
|
+
expect(host.input.haptics.vibrateWaveform).toBeUndefined();
|
|
217
|
+
expect(vibrateDeviceWaveform(host, [10, 20], [255, 128])).toBe(true);
|
|
218
|
+
expect(calls[0]).toEqual({ args: [[10, 20]], name: 'vibratePattern' });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('rejects empty timings before reaching either path', () => {
|
|
222
|
+
const { calls, host } = makeHost();
|
|
223
|
+
expect(vibrateDeviceWaveform(host, [], [])).toBe(false);
|
|
224
|
+
expect(calls).toEqual([]);
|
|
425
225
|
});
|
|
426
226
|
});
|