Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 43 additions & 22 deletions packages/core/src/device/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,8 @@ export const ActionSwipeParamSchema = z.object({
),
distance: z
.number()
.finite()
.positive()
.optional()
.describe('The distance in pixels to swipe (mutually exclusive with end)'),
end: getMidsceneLocationSchema()
Expand All @@ -769,13 +771,18 @@ export const ActionSwipeParamSchema = z.object({
),
duration: z
.number()
.finite()
.positive()
.default(300)
.describe('Duration of the swipe gesture in milliseconds'),
repeat: z
.number()
.finite()
.int()
.nonnegative()
.optional()
.describe(
'The number of times to repeat the swipe gesture. 1 for default, 0 for infinite (e.g. endless swipe until the end of the page)',
'The number of times to repeat the swipe gesture. 1 for default, 0 for continuous mode capped at 10 repeats',
),
});

Expand All @@ -798,7 +805,31 @@ export function normalizeMobileSwipeParam(
repeatCount: number;
} {
const { width, height } = screenSize;
const { start, end } = param;
const { start, end, distance, direction } = param;

if (end !== undefined && distance !== undefined) {
throw new Error('end and distance are mutually exclusive');
}
if (distance !== undefined && (!Number.isFinite(distance) || distance <= 0)) {
throw new Error('distance must be a positive finite number');
}
if (distance !== undefined && direction === undefined) {
throw new Error('direction is required when using distance');
}

const duration = param.duration ?? 300;
if (!Number.isFinite(duration) || duration <= 0) {
throw new Error('duration must be a positive finite number');
}

const repeatCount = param.repeat ?? 1;
if (
!Number.isFinite(repeatCount) ||
!Number.isInteger(repeatCount) ||
repeatCount < 0
) {
throw new Error('repeat must be a non-negative finite integer');
}

const startPoint = start
? { x: start.center[0], y: start.center[1] }
Expand All @@ -808,26 +839,18 @@ export function normalizeMobileSwipeParam(

if (end) {
endPoint = { x: end.center[0], y: end.center[1] };
} else if (param.distance) {
const direction = param.direction;
if (!direction) {
throw new Error('direction is required for swipe gesture');
}
} else if (distance !== undefined) {
endPoint = {
x:
startPoint.x +
(direction === 'right'
? param.distance
? distance
: direction === 'left'
? -param.distance
? -distance
: 0),
y:
startPoint.y +
(direction === 'down'
? param.distance
: direction === 'up'
? -param.distance
: 0),
(direction === 'down' ? distance : direction === 'up' ? -distance : 0),
};
} else {
throw new Error(
Expand All @@ -838,14 +861,12 @@ export function normalizeMobileSwipeParam(
endPoint.x = Math.max(0, Math.min(endPoint.x, width));
endPoint.y = Math.max(0, Math.min(endPoint.y, height));

const duration = param.duration ?? 300;

let repeatCount = typeof param.repeat === 'number' ? param.repeat : 1;
if (repeatCount === 0) {
repeatCount = 10;
}

return { startPoint, endPoint, duration, repeatCount };
return {
startPoint,
endPoint,
duration,
repeatCount: repeatCount === 0 ? 10 : repeatCount,
};
}

export const defineActionSwipe = (config: {
Expand Down
167 changes: 167 additions & 0 deletions packages/core/tests/unit-test/device/swipe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { ActionSwipeParamSchema, normalizeMobileSwipeParam } from '@/device';
import type { LocateResultElement } from '@/types';
import { describe, expect, it } from 'vitest';

const screenSize = { width: 1000, height: 800 };

const locatedAt = (x: number, y: number): LocateResultElement =>
({
center: [x, y],
}) as LocateResultElement;

describe('ActionSwipeParamSchema', () => {
it.each([
{
name: 'negative distance',
param: { distance: -100, direction: 'right' },
},
{
name: 'zero distance',
param: { distance: 0, direction: 'right' },
},
{
name: 'negative duration',
param: { end: { prompt: 'right edge' }, duration: -1 },
},
{
name: 'non-finite duration',
param: {
end: { prompt: 'right edge' },
duration: Number.POSITIVE_INFINITY,
},
},
{
name: 'negative repeat',
param: { end: { prompt: 'right edge' }, repeat: -1 },
},
{
name: 'fractional repeat',
param: { end: { prompt: 'right edge' }, repeat: 1.5 },
},
{
name: 'non-finite repeat',
param: {
end: { prompt: 'right edge' },
repeat: Number.POSITIVE_INFINITY,
},
},
])('rejects $name', ({ param }) => {
expect(ActionSwipeParamSchema.safeParse(param).success).toBe(false);
});

it('accepts distance with direction and a non-negative integer repeat', () => {
expect(
ActionSwipeParamSchema.safeParse({
distance: 100,
direction: 'right',
repeat: 0,
}).success,
).toBe(true);
});
});

describe('normalizeMobileSwipeParam', () => {
it('normalizes a valid relative swipe', () => {
expect(
normalizeMobileSwipeParam(
{
start: locatedAt(500, 400),
distance: 100,
direction: 'right',
duration: 200,
repeat: 2,
},
screenSize,
),
).toEqual({
startPoint: { x: 500, y: 400 },
endPoint: { x: 600, y: 400 },
duration: 200,
repeatCount: 2,
});
});

it.each([
{
name: 'negative distance',
param: { distance: -100, direction: 'right' as const },
message: 'distance must be a positive finite number',
},
{
name: 'zero distance',
param: { distance: 0, direction: 'right' as const },
message: 'distance must be a positive finite number',
},
{
name: 'non-finite distance',
param: {
distance: Number.POSITIVE_INFINITY,
direction: 'right' as const,
},
message: 'distance must be a positive finite number',
},
{
name: 'missing direction',
param: { distance: 100 },
message: 'direction is required when using distance',
},
{
name: 'negative duration',
param: { end: locatedAt(600, 400), duration: -1 },
message: 'duration must be a positive finite number',
},
{
name: 'non-finite duration',
param: {
end: locatedAt(600, 400),
duration: Number.POSITIVE_INFINITY,
},
message: 'duration must be a positive finite number',
},
{
name: 'negative repeat',
param: { end: locatedAt(600, 400), repeat: -1 },
message: 'repeat must be a non-negative finite integer',
},
{
name: 'fractional repeat',
param: { end: locatedAt(600, 400), repeat: 1.5 },
message: 'repeat must be a non-negative finite integer',
},
{
name: 'non-finite repeat',
param: {
end: locatedAt(600, 400),
repeat: Number.POSITIVE_INFINITY,
},
message: 'repeat must be a non-negative finite integer',
},
])('rejects $name', ({ param, message }) => {
expect(() => normalizeMobileSwipeParam(param, screenSize)).toThrow(message);
});

it('rejects end and distance together', () => {
expect(() =>
normalizeMobileSwipeParam(
{
end: locatedAt(600, 400),
distance: 100,
direction: 'right',
},
screenSize,
),
).toThrow('end and distance are mutually exclusive');
});

it('keeps zero repeat as the capped continuous mode', () => {
expect(
normalizeMobileSwipeParam(
{
end: locatedAt(600, 400),
repeat: 0,
},
screenSize,
).repeatCount,
).toBe(10);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ describe('buildActionDescription and serializeActionDescriptions', () => {
repeat:
type: number
optional: true
description: The number of times to repeat the swipe gesture. 1 for default, 0 for infinite (e.g. endless swipe until the end of the page)
description: The number of times to repeat the swipe gesture. 1 for default, 0 for continuous mode capped at 10 repeats
sample: |-
<action-type>Swipe</action-type>
<action-param-json>
Expand Down
Loading