mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
refactor: restructure repo for cleaner GitHub landing page
Reduce visible top-level items from 21 to 14: - Untrack test-results/, tmp/, public symlink (added to .gitignore) - Move agent-teams/ → docs/agent-teams/ - Move mobile-test/ → test/mobile/ - Move tools/remotion/ → scripts/remotion/ - Move eslint.config.js, vitest.config.ts → config/ All path references updated across CLAUDE.md, package.json, .prettierignore, vitest configs, and capture scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
import type { Page } from 'playwright';
|
||||
import { getCDP, dispatchTouchEvent } from './cdp.js';
|
||||
import { SWIPE } from './constants.js';
|
||||
|
||||
export type SwipeDirection = 'left' | 'right' | 'up' | 'down';
|
||||
|
||||
export interface SwipeOptions {
|
||||
/** Distance in pixels (default: SWIPE.MIN_DISTANCE + 20 = 100) */
|
||||
distance?: number;
|
||||
/** Duration in ms (default: 150, must be < SWIPE.MAX_TIME) */
|
||||
duration?: number;
|
||||
/** Number of intermediate touch move points (default: 5) */
|
||||
steps?: number;
|
||||
/** Start position override (default: center of target element) */
|
||||
startPoint?: { x: number; y: number };
|
||||
}
|
||||
|
||||
// ─── CDP Touch (Chromium, trusted events) ───
|
||||
|
||||
/** Perform a swipe gesture via CDP Input.dispatchTouchEvent (trusted, Chromium only).
|
||||
* Target element defaults to '.main' (where SwipeHandler listens). */
|
||||
export async function swipeViaCDP(
|
||||
page: Page,
|
||||
direction: SwipeDirection,
|
||||
options: SwipeOptions = {},
|
||||
): Promise<void> {
|
||||
const {
|
||||
distance = SWIPE.MIN_DISTANCE + 20,
|
||||
duration = 150,
|
||||
steps = 5,
|
||||
} = options;
|
||||
|
||||
const cdp = await getCDP(page);
|
||||
const targetSelector = '.main';
|
||||
const box = await page.locator(targetSelector).boundingBox();
|
||||
if (!box) throw new Error(`Element ${targetSelector} not found`);
|
||||
|
||||
let startX: number, startY: number, endX: number, endY: number;
|
||||
|
||||
if (options.startPoint) {
|
||||
startX = options.startPoint.x;
|
||||
startY = options.startPoint.y;
|
||||
} else {
|
||||
startX = box.x + box.width / 2;
|
||||
startY = box.y + box.height / 2;
|
||||
}
|
||||
|
||||
// Calculate end point based on direction
|
||||
switch (direction) {
|
||||
case 'left':
|
||||
startX = box.x + box.width * 0.75;
|
||||
endX = startX - distance;
|
||||
endY = startY;
|
||||
break;
|
||||
case 'right':
|
||||
startX = box.x + box.width * 0.25;
|
||||
endX = startX + distance;
|
||||
endY = startY;
|
||||
break;
|
||||
case 'up':
|
||||
endX = startX;
|
||||
endY = startY - distance;
|
||||
break;
|
||||
case 'down':
|
||||
endX = startX;
|
||||
endY = startY + distance;
|
||||
break;
|
||||
}
|
||||
|
||||
// Touch start
|
||||
await dispatchTouchEvent(cdp, 'touchStart', [{ x: startX, y: startY }]);
|
||||
|
||||
// Intermediate moves
|
||||
const stepDelay = duration / steps;
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
const progress = i / steps;
|
||||
const x = startX + (endX! - startX) * progress;
|
||||
const y = startY + (endY! - startY) * progress;
|
||||
await dispatchTouchEvent(cdp, 'touchMove', [{ x, y }]);
|
||||
await page.waitForTimeout(stepDelay);
|
||||
}
|
||||
|
||||
// Touch end
|
||||
await dispatchTouchEvent(cdp, 'touchEnd', []);
|
||||
}
|
||||
|
||||
// ─── Synthetic Touch (cross-engine fallback) ───
|
||||
|
||||
/** Perform a swipe gesture via synthetic TouchEvent dispatch.
|
||||
* Events have isTrusted=false but still trigger handler callbacks. */
|
||||
export async function swipeViaSynthetic(
|
||||
page: Page,
|
||||
direction: SwipeDirection,
|
||||
options: SwipeOptions = {},
|
||||
): Promise<void> {
|
||||
const {
|
||||
distance = SWIPE.MIN_DISTANCE + 20,
|
||||
duration = 150,
|
||||
steps = 5,
|
||||
} = options;
|
||||
|
||||
await page.evaluate(
|
||||
({ dir, dist, dur, numSteps }) => {
|
||||
const main = document.querySelector('.main');
|
||||
if (!main) return;
|
||||
|
||||
const rect = main.getBoundingClientRect();
|
||||
let startX = rect.left + rect.width / 2;
|
||||
let startY = rect.top + rect.height / 2;
|
||||
let endX = startX;
|
||||
let endY = startY;
|
||||
|
||||
switch (dir) {
|
||||
case 'left':
|
||||
startX = rect.left + rect.width * 0.75;
|
||||
endX = startX - dist;
|
||||
break;
|
||||
case 'right':
|
||||
startX = rect.left + rect.width * 0.25;
|
||||
endX = startX + dist;
|
||||
break;
|
||||
case 'up':
|
||||
endY = startY - dist;
|
||||
break;
|
||||
case 'down':
|
||||
endY = startY + dist;
|
||||
break;
|
||||
}
|
||||
|
||||
function createTouch(x: number, y: number) {
|
||||
return new Touch({
|
||||
identifier: 0,
|
||||
target: main!,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
pageX: x,
|
||||
pageY: y,
|
||||
});
|
||||
}
|
||||
|
||||
// touchstart
|
||||
main.dispatchEvent(new TouchEvent('touchstart', {
|
||||
touches: [createTouch(startX, startY)],
|
||||
changedTouches: [createTouch(startX, startY)],
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}));
|
||||
|
||||
// Intermediate moves
|
||||
const stepDelay = dur / numSteps;
|
||||
for (let i = 1; i <= numSteps; i++) {
|
||||
const progress = i / numSteps;
|
||||
const x = startX + (endX - startX) * progress;
|
||||
const y = startY + (endY - startY) * progress;
|
||||
setTimeout(() => {
|
||||
main.dispatchEvent(new TouchEvent('touchmove', {
|
||||
touches: [createTouch(x, y)],
|
||||
changedTouches: [createTouch(x, y)],
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}));
|
||||
}, stepDelay * i);
|
||||
}
|
||||
|
||||
// touchend
|
||||
setTimeout(() => {
|
||||
main.dispatchEvent(new TouchEvent('touchend', {
|
||||
touches: [],
|
||||
changedTouches: [createTouch(endX, endY)],
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}));
|
||||
}, dur + 10);
|
||||
},
|
||||
{ dir: direction, dist: distance, dur: duration, numSteps: steps },
|
||||
);
|
||||
|
||||
// Wait for the full gesture + a little buffer
|
||||
await page.waitForTimeout(duration + 100);
|
||||
}
|
||||
|
||||
// ─── Unified API ───
|
||||
|
||||
/** Perform a swipe gesture using the best available method.
|
||||
* Uses CDP for Chromium (trusted events), synthetic for WebKit. */
|
||||
export async function swipe(
|
||||
page: Page,
|
||||
direction: SwipeDirection,
|
||||
options: SwipeOptions & { isChromium?: boolean } = {},
|
||||
): Promise<void> {
|
||||
const { isChromium = true, ...swipeOpts } = options;
|
||||
|
||||
if (isChromium) {
|
||||
try {
|
||||
await swipeViaCDP(page, direction, swipeOpts);
|
||||
return;
|
||||
} catch {
|
||||
// Fall through to synthetic
|
||||
}
|
||||
}
|
||||
|
||||
await swipeViaSynthetic(page, direction, swipeOpts);
|
||||
}
|
||||
|
||||
/** Tap an element via CDP (trusted touch) */
|
||||
export async function tapViaCDP(page: Page, selector: string): Promise<void> {
|
||||
const cdp = await getCDP(page);
|
||||
const box = await page.locator(selector).boundingBox();
|
||||
if (!box) throw new Error(`Element ${selector} not found for tap`);
|
||||
|
||||
const x = box.x + box.width / 2;
|
||||
const y = box.y + box.height / 2;
|
||||
|
||||
await dispatchTouchEvent(cdp, 'touchStart', [{ x, y }]);
|
||||
await page.waitForTimeout(50);
|
||||
await dispatchTouchEvent(cdp, 'touchEnd', []);
|
||||
}
|
||||
|
||||
/** Tap an element via synthetic touch (cross-engine) */
|
||||
export async function tapViaSynthetic(page: Page, selector: string): Promise<void> {
|
||||
await page.evaluate((sel) => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + rect.width / 2;
|
||||
const y = rect.top + rect.height / 2;
|
||||
const touch = new Touch({
|
||||
identifier: 0,
|
||||
target: el,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
});
|
||||
el.dispatchEvent(new TouchEvent('touchstart', {
|
||||
touches: [touch],
|
||||
changedTouches: [touch],
|
||||
bubbles: true,
|
||||
}));
|
||||
el.dispatchEvent(new TouchEvent('touchend', {
|
||||
touches: [],
|
||||
changedTouches: [touch],
|
||||
bubbles: true,
|
||||
}));
|
||||
}, selector);
|
||||
}
|
||||
|
||||
/** Tap using the best available method */
|
||||
export async function tap(
|
||||
page: Page,
|
||||
selector: string,
|
||||
options: { isChromium?: boolean } = {},
|
||||
): Promise<void> {
|
||||
if (options.isChromium !== false) {
|
||||
try {
|
||||
await tapViaCDP(page, selector);
|
||||
return;
|
||||
} catch {
|
||||
// Fall through
|
||||
}
|
||||
}
|
||||
await tapViaSynthetic(page, selector);
|
||||
}
|
||||
Reference in New Issue
Block a user