* Copyright (c) 2025 Huawei Technologies Co., Ltd.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {HdcClient} from './HdcClient';
import {PixelRatio} from 'react-native';
import {Tracing} from './Tracing';
interface Measurable {
measure: (
callback: (
x: number,
y: number,
width: number,
height: number,
pageX: number,
pageY: number,
) => void,
) => void;
}
function isMeasurable(ref: any): ref is Measurable {
return ref && typeof ref.measure === 'function';
}
enum UIDirectionEnum {
LEFT = 0,
RIGHT = 1,
DOWN = 2,
UP = 3,
}
interface Offset {
x: number;
y: number;
}
export class DriverError extends Error {}
export type UIDirection = 'LEFT' | 'RIGHT' | 'DOWN' | 'UP';
const HARMONY_KEY_IDS = {
Backspace: 2055,
Enter: 2054,
} as const;
type SupportedKey = keyof typeof HARMONY_KEY_IDS;
export class Driver {
private hdcClient: HdcClient;
static async create(
url: string = 'localhost',
port: number = 8083,
): Promise<Driver> {
const hdcClient = await HdcClient.create(url, port);
return new Driver(hdcClient);
}
private constructor(hdcClient: HdcClient) {
this.hdcClient = hdcClient;
}
private dpToPhysical(coord: number): number {
return PixelRatio.getPixelSizeForLayoutSize(Math.round(coord));
}
async fling({
direction,
speed = 600,
}: {
direction: UIDirection;
speed?: number;
}): Promise<void> {
this.hdcClient
.uiTest()
.uiInput()
.dircFling(UIDirectionEnum[direction], speed);
}
async swipe({
ref,
fromOffset,
toOffset,
speed = 600,
}: {
ref: React.RefObject<any>;
fromOffset: Offset;
toOffset: Offset;
speed?: number;
}): Promise<void> {
const {x: fx, y: fy} = await this.getAbsolutePosition(ref, fromOffset);
const {x: tx, y: ty} = await this.getAbsolutePosition(ref, toOffset);
this.hdcClient.uiTest().uiInput().swipe(fx, fy, tx, ty, speed);
}
private getAbsolutePosition(
ref: React.RefObject<any>,
offset: Offset = {x: 0, y: 0},
): Promise<{x: number; y: number}> {
return new Promise((resolve, reject) => {
if (!ref.current) {
return reject(new DriverError('Ref is undefined'));
}
if (!isMeasurable(ref.current)) {
return reject(
new DriverError('Component does not have measure method'),
);
}
ref.current.measure((_, __, width, height, pageX, pageY) => {
resolve({
x: this.dpToPhysical(pageX + width / 2 + offset.x),
y: this.dpToPhysical(pageY + height / 2 + offset.y),
});
});
});
}
async click({
ref,
offset,
}: {
ref: React.RefObject<any>;
offset?: Offset;
}): Promise<void> {
const {x, y} = await this.getAbsolutePosition(ref, offset);
await this.hdcClient.uiTest().uiInput().click(x, y);
}
async longClick({
ref,
offset,
}: {
ref: React.RefObject<any>;
offset?: Offset;
}): Promise<void> {
const {x, y} = await this.getAbsolutePosition(ref, offset);
await this.hdcClient.uiTest().uiInput().longclick(x, y);
}
keyEvent() {
const keyEventInstance = this.hdcClient.uiTest().uiInput().keyEvent();
const keyEventChain = {
back: () => {
keyEventInstance.back();
return keyEventChain;
},
home: () => {
keyEventInstance.home();
return keyEventChain;
},
power: () => {
keyEventInstance.power();
return keyEventChain;
},
key: (key: SupportedKey) => {
keyEventInstance.keyId(HARMONY_KEY_IDS[key]);
return keyEventChain;
},
send: async () => {
await keyEventInstance.send();
},
};
return keyEventChain;
}
async inputText(ref: React.RefObject<any>, text: string) {
const {x, y} = await this.getAbsolutePosition(ref);
await this.hdcClient.uiTest().uiInput().inputText(x, y, text);
}
async beginTracing() {
await this.hdcClient.hitrace().begin('app');
return new Tracing(this.hdcClient);
}
async getDeviceModel(): Promise<string> {
return this.hdcClient.getSystemParam('const.product.model');
}
async getDeviceVersion(): Promise<string> {
return this.hdcClient.getSystemParam('const.ohos.fullname');
}
}