Files
Genarrative/apps/mobile-shell/src/host-bridge/haptics.test.ts
T
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

123 lines
3.4 KiB
TypeScript

import * as Haptics from 'expo-haptics';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import {
HOST_BRIDGE_PROTOCOL,
HOST_BRIDGE_VERSION,
type HostBridgeRequest,
} from '../../../../packages/shared/src/contracts/hostBridge';
import {
runMobileHapticsImpact,
runMobileHostBridgeHapticsImpact,
} from './haptics';
vi.mock('expo-haptics', () => ({
ImpactFeedbackStyle: {
Heavy: 'heavy',
Light: 'light',
Medium: 'medium',
},
impactAsync: vi.fn(),
}));
function request(payload?: unknown): HostBridgeRequest {
return {
bridge: HOST_BRIDGE_PROTOCOL,
version: HOST_BRIDGE_VERSION,
id: 'haptics-request',
method: 'haptics.impact',
payload,
};
}
beforeEach(() => {
vi.mocked(Haptics.impactAsync).mockReset();
});
describe('mobile haptics helpers', () => {
test('maps allowed impact styles to Expo Haptics styles', async () => {
await expect(runMobileHapticsImpact('light')).resolves.toBe(true);
expect(Haptics.impactAsync).toHaveBeenLastCalledWith(
Haptics.ImpactFeedbackStyle.Light,
);
await expect(runMobileHapticsImpact('medium')).resolves.toBe(true);
expect(Haptics.impactAsync).toHaveBeenLastCalledWith(
Haptics.ImpactFeedbackStyle.Medium,
);
await expect(runMobileHapticsImpact('heavy')).resolves.toBe(true);
expect(Haptics.impactAsync).toHaveBeenLastCalledWith(
Haptics.ImpactFeedbackStyle.Heavy,
);
});
test('defaults missing style to light impact', async () => {
await expect(runMobileHapticsImpact(undefined)).resolves.toBe(true);
expect(Haptics.impactAsync).toHaveBeenCalledWith(
Haptics.ImpactFeedbackStyle.Light,
);
});
test('rejects unknown styles without triggering device feedback', async () => {
await expect(runMobileHapticsImpact('rigid')).resolves.toBe(false);
expect(Haptics.impactAsync).not.toHaveBeenCalled();
});
test('reports unavailable native feedback with a stable HostBridge error', async () => {
const warnSpy = vi
.spyOn(console, 'warn')
.mockImplementation(() => undefined);
const nativeError = new Error('native haptics failed');
vi.mocked(Haptics.impactAsync).mockRejectedValueOnce(nativeError);
try {
await expect(runMobileHapticsImpact('light')).rejects.toMatchObject({
code: 'host_error',
message: 'haptics impact unavailable',
});
expect(warnSpy).toHaveBeenCalledWith(
'mobile haptics failed for impact.dispatch',
);
} finally {
warnSpy.mockRestore();
}
});
test('wraps HostBridge success response after real impact dispatch', async () => {
const response = await runMobileHostBridgeHapticsImpact(
request({
style: 'medium',
}),
);
expect(response).toEqual({
bridge: HOST_BRIDGE_PROTOCOL,
version: HOST_BRIDGE_VERSION,
id: 'haptics-request',
ok: true,
result: true,
});
expect(Haptics.impactAsync).toHaveBeenCalledWith(
Haptics.ImpactFeedbackStyle.Medium,
);
});
test('wraps HostBridge invalid request before device feedback', async () => {
await expect(
runMobileHostBridgeHapticsImpact(
request({
style: 'rigid',
}),
),
).rejects.toMatchObject({
code: 'invalid_request',
message: 'haptics impact style must be light, medium, or heavy',
});
expect(Haptics.impactAsync).not.toHaveBeenCalled();
});
});