@epsilon-asi/actors 0.0.22 → 0.0.33
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/dist/browser/RuntimeConfig.d.ts +26 -0
- package/dist/browser/RuntimeConfig.d.ts.map +1 -1
- package/dist/browser/RuntimeConfig.js +29 -1
- package/dist/browser/RuntimeConfig.js.map +1 -1
- package/dist/core/ActorContext.d.ts +2 -0
- package/dist/core/ActorContext.d.ts.map +1 -1
- package/dist/core/ActorRunner.d.ts +3 -0
- package/dist/core/ActorRunner.d.ts.map +1 -1
- package/dist/core/ActorRunner.js +11 -1
- package/dist/core/ActorRunner.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/native/CompositeNativeWindowDriver.d.ts +11 -0
- package/dist/native/CompositeNativeWindowDriver.d.ts.map +1 -0
- package/dist/native/CompositeNativeWindowDriver.js +31 -0
- package/dist/native/CompositeNativeWindowDriver.js.map +1 -0
- package/dist/native/NativeActionRegistry.d.ts +14 -0
- package/dist/native/NativeActionRegistry.d.ts.map +1 -0
- package/dist/native/NativeActionRegistry.js +101 -0
- package/dist/native/NativeActionRegistry.js.map +1 -0
- package/dist/native/NativeAutomation.d.ts +3 -0
- package/dist/native/NativeAutomation.d.ts.map +1 -0
- package/dist/native/NativeAutomation.js +12 -0
- package/dist/native/NativeAutomation.js.map +1 -0
- package/dist/native/NativeCoordinateMapper.d.ts +23 -0
- package/dist/native/NativeCoordinateMapper.d.ts.map +1 -0
- package/dist/native/NativeCoordinateMapper.js +201 -0
- package/dist/native/NativeCoordinateMapper.js.map +1 -0
- package/dist/native/NativeFileDialogService.d.ts +26 -0
- package/dist/native/NativeFileDialogService.d.ts.map +1 -0
- package/dist/native/NativeFileDialogService.js +121 -0
- package/dist/native/NativeFileDialogService.js.map +1 -0
- package/dist/native/NativeImageFinder.d.ts +12 -0
- package/dist/native/NativeImageFinder.d.ts.map +1 -0
- package/dist/native/NativeImageFinder.js +29 -0
- package/dist/native/NativeImageFinder.js.map +1 -0
- package/dist/native/NativeKeyboard.d.ts +10 -0
- package/dist/native/NativeKeyboard.d.ts.map +1 -0
- package/dist/native/NativeKeyboard.js +16 -0
- package/dist/native/NativeKeyboard.js.map +1 -0
- package/dist/native/NativeMouse.d.ts +38 -0
- package/dist/native/NativeMouse.d.ts.map +1 -0
- package/dist/native/NativeMouse.js +82 -0
- package/dist/native/NativeMouse.js.map +1 -0
- package/dist/native/NativeWindowService.d.ts +31 -0
- package/dist/native/NativeWindowService.d.ts.map +1 -0
- package/dist/native/NativeWindowService.js +183 -0
- package/dist/native/NativeWindowService.js.map +1 -0
- package/dist/native/UnsupportedNativeAutomation.d.ts +4 -0
- package/dist/native/UnsupportedNativeAutomation.d.ts.map +1 -0
- package/dist/native/UnsupportedNativeAutomation.js +77 -0
- package/dist/native/UnsupportedNativeAutomation.js.map +1 -0
- package/dist/native/WindowMatcher.d.ts +4 -0
- package/dist/native/WindowMatcher.d.ts.map +1 -0
- package/dist/native/WindowMatcher.js +39 -0
- package/dist/native/WindowMatcher.js.map +1 -0
- package/dist/native/drivers.d.ts +37 -0
- package/dist/native/drivers.d.ts.map +1 -0
- package/dist/native/drivers.js +2 -0
- package/dist/native/drivers.js.map +1 -0
- package/dist/native/errors.d.ts +23 -0
- package/dist/native/errors.d.ts.map +1 -0
- package/dist/native/errors.js +45 -0
- package/dist/native/errors.js.map +1 -0
- package/dist/native/index.d.ts +13 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +13 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/macos/MacOSAccessibilityWindowDriver.d.ts +11 -0
- package/dist/native/macos/MacOSAccessibilityWindowDriver.d.ts.map +1 -0
- package/dist/native/macos/MacOSAccessibilityWindowDriver.js +180 -0
- package/dist/native/macos/MacOSAccessibilityWindowDriver.js.map +1 -0
- package/dist/native/macos/MacOSAppleScriptClient.d.ts +24 -0
- package/dist/native/macos/MacOSAppleScriptClient.d.ts.map +1 -0
- package/dist/native/macos/MacOSAppleScriptClient.js +163 -0
- package/dist/native/macos/MacOSAppleScriptClient.js.map +1 -0
- package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.d.ts +10 -0
- package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.d.ts.map +1 -0
- package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.js +12 -0
- package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.js.map +1 -0
- package/dist/native/macos/MacOSNativeAutomation.d.ts +3 -0
- package/dist/native/macos/MacOSNativeAutomation.d.ts.map +1 -0
- package/dist/native/macos/MacOSNativeAutomation.js +88 -0
- package/dist/native/macos/MacOSNativeAutomation.js.map +1 -0
- package/dist/native/nut/NutNativeImageFinder.d.ts +17 -0
- package/dist/native/nut/NutNativeImageFinder.d.ts.map +1 -0
- package/dist/native/nut/NutNativeImageFinder.js +84 -0
- package/dist/native/nut/NutNativeImageFinder.js.map +1 -0
- package/dist/native/nut/NutNativeKeyboardDriver.d.ts +8 -0
- package/dist/native/nut/NutNativeKeyboardDriver.d.ts.map +1 -0
- package/dist/native/nut/NutNativeKeyboardDriver.js +39 -0
- package/dist/native/nut/NutNativeKeyboardDriver.js.map +1 -0
- package/dist/native/nut/NutNativeMouseDriver.d.ts +8 -0
- package/dist/native/nut/NutNativeMouseDriver.d.ts.map +1 -0
- package/dist/native/nut/NutNativeMouseDriver.js +24 -0
- package/dist/native/nut/NutNativeMouseDriver.js.map +1 -0
- package/dist/native/nut/NutNativeScreenDriver.d.ts +6 -0
- package/dist/native/nut/NutNativeScreenDriver.d.ts.map +1 -0
- package/dist/native/nut/NutNativeScreenDriver.js +12 -0
- package/dist/native/nut/NutNativeScreenDriver.js.map +1 -0
- package/dist/native/nut/NutNativeWindowDriver.d.ts +6 -0
- package/dist/native/nut/NutNativeWindowDriver.d.ts.map +1 -0
- package/dist/native/nut/NutNativeWindowDriver.js +53 -0
- package/dist/native/nut/NutNativeWindowDriver.js.map +1 -0
- package/dist/native/nut/loadNut.d.ts +58 -0
- package/dist/native/nut/loadNut.d.ts.map +1 -0
- package/dist/native/nut/loadNut.js +25 -0
- package/dist/native/nut/loadNut.js.map +1 -0
- package/dist/native/types.d.ts +194 -0
- package/dist/native/types.d.ts.map +1 -0
- package/dist/native/types.js +2 -0
- package/dist/native/types.js.map +1 -0
- package/dist/native/utils/appleScriptEscape.d.ts +7 -0
- package/dist/native/utils/appleScriptEscape.d.ts.map +1 -0
- package/dist/native/utils/appleScriptEscape.js +11 -0
- package/dist/native/utils/appleScriptEscape.js.map +1 -0
- package/dist/native/utils/geometry.d.ts +12 -0
- package/dist/native/utils/geometry.d.ts.map +1 -0
- package/dist/native/utils/geometry.js +77 -0
- package/dist/native/utils/geometry.js.map +1 -0
- package/dist/native/utils/redactNative.d.ts +2 -0
- package/dist/native/utils/redactNative.d.ts.map +1 -0
- package/dist/native/utils/redactNative.js +7 -0
- package/dist/native/utils/redactNative.js.map +1 -0
- package/dist/native/utils/waitFor.d.ts +7 -0
- package/dist/native/utils/waitFor.d.ts.map +1 -0
- package/dist/native/utils/waitFor.js +17 -0
- package/dist/native/utils/waitFor.js.map +1 -0
- package/dist/sites/upwork-com/upwork-com.actor.d.ts +4 -1
- package/dist/sites/upwork-com/upwork-com.actor.d.ts.map +1 -1
- package/dist/sites/upwork-com/upwork-com.actor.js +31 -11
- package/dist/sites/upwork-com/upwork-com.actor.js.map +1 -1
- package/dist/sites/upwork-com/upwork-com.types.d.ts +3 -1
- package/dist/sites/upwork-com/upwork-com.types.d.ts.map +1 -1
- package/dist/sites/upwork-com/upwork-com.types.js.map +1 -1
- package/dist/sites/upwork-com/util/parseJobApplicationDetails.d.ts +70 -0
- package/dist/sites/upwork-com/util/parseJobApplicationDetails.d.ts.map +1 -0
- package/dist/sites/upwork-com/util/parseJobApplicationDetails.js +334 -0
- package/dist/sites/upwork-com/util/parseJobApplicationDetails.js.map +1 -0
- package/npmrc +5 -0
- package/package.json +5 -1
- package/src/browser/RuntimeConfig.ts +57 -1
- package/src/core/ActorContext.ts +2 -0
- package/src/core/ActorRunner.ts +13 -1
- package/src/index.ts +2 -0
- package/src/native/CompositeNativeWindowDriver.ts +30 -0
- package/src/native/NativeActionRegistry.ts +114 -0
- package/src/native/NativeAutomation.ts +15 -0
- package/src/native/NativeCoordinateMapper.ts +258 -0
- package/src/native/NativeFileDialogService.ts +138 -0
- package/src/native/NativeImageFinder.ts +33 -0
- package/src/native/NativeKeyboard.ts +18 -0
- package/src/native/NativeMouse.ts +116 -0
- package/src/native/NativeWindowService.ts +229 -0
- package/src/native/UnsupportedNativeAutomation.ts +92 -0
- package/src/native/WindowMatcher.ts +31 -0
- package/src/native/drivers.ts +38 -0
- package/src/native/errors.ts +51 -0
- package/src/native/index.ts +12 -0
- package/src/native/macos/MacOSAccessibilityWindowDriver.ts +183 -0
- package/src/native/macos/MacOSAppleScriptClient.ts +182 -0
- package/src/native/macos/MacOSFileDialogAccessibilityStrategy.ts +11 -0
- package/src/native/macos/MacOSNativeAutomation.ts +86 -0
- package/src/native/nut/NutNativeImageFinder.ts +98 -0
- package/src/native/nut/NutNativeKeyboardDriver.ts +38 -0
- package/src/native/nut/NutNativeMouseDriver.ts +27 -0
- package/src/native/nut/NutNativeScreenDriver.ts +14 -0
- package/src/native/nut/NutNativeWindowDriver.ts +61 -0
- package/src/native/nut/loadNut.ts +86 -0
- package/src/native/types.ts +224 -0
- package/src/native/utils/appleScriptEscape.ts +11 -0
- package/src/native/utils/geometry.ts +88 -0
- package/src/native/utils/redactNative.ts +6 -0
- package/src/native/utils/waitFor.ts +25 -0
- package/src/sites/upwork-com/upwork-com.actor.ts +47 -14
- package/src/sites/upwork-com/upwork-com.types.ts +4 -1
- package/src/sites/upwork-com/util/parseJobApplicationDetails.ts +622 -0
- package/tests/fixtures/makeContext.ts +7 -2
- package/tests/fixtures/native/FakeNativeAutomation.ts +138 -0
- package/tests/unit/browser/RuntimeConfig.native.test.ts +63 -0
- package/tests/unit/core/ActorRunner.native.test.ts +69 -0
- package/tests/unit/native/MacOSAppleScriptClient.test.ts +35 -0
- package/tests/unit/native/NativeActionRegistry.test.ts +34 -0
- package/tests/unit/native/NativeCoordinateMapper.test.ts +92 -0
- package/tests/unit/native/NativeFileDialogService.test.ts +91 -0
- package/tests/unit/native/NativeMouse.test.ts +91 -0
- package/tests/unit/native/NativeWindowService.test.ts +87 -0
- package/tests/unit/native/WindowMatcher.test.ts +32 -0
- package/tests/unit/native/appleScriptEscape.test.ts +9 -0
- package/tests/unit/sites/myvistage-com.login.test.ts +1 -1
- package/tests/unit/sites/myvistage-com.postComment.test.ts +0 -1
- package/tests/unit/sites/upwork-com.login.test.ts +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { PageLike } from '../../../src/browser/PuppeteerLike.js';
|
|
2
|
+
import { DefaultNativeActionRegistry, registerDefaultNativeActions } from '../../../src/native/NativeActionRegistry.js';
|
|
3
|
+
import type {
|
|
4
|
+
BrowserPoint,
|
|
5
|
+
CalibrationOptions,
|
|
6
|
+
CoordinateMapOptions,
|
|
7
|
+
CoordinateTransform,
|
|
8
|
+
NativeAutomation,
|
|
9
|
+
NativeFileDialogService,
|
|
10
|
+
NativeImageFinder,
|
|
11
|
+
NativeImageMatch,
|
|
12
|
+
NativeImageSearchOptions,
|
|
13
|
+
NativeKeyboard,
|
|
14
|
+
NativeMouse,
|
|
15
|
+
NativeMouseClickOptions,
|
|
16
|
+
NativeMouseMoveOptions,
|
|
17
|
+
NativeRegion,
|
|
18
|
+
NativeTemplateRef,
|
|
19
|
+
NativeWindowInfo,
|
|
20
|
+
NativeWindowService,
|
|
21
|
+
ScreenPoint,
|
|
22
|
+
SelectFileOptions,
|
|
23
|
+
WaitOptions,
|
|
24
|
+
WindowButtonOptions,
|
|
25
|
+
WindowMatcher
|
|
26
|
+
} from '../../../src/native/types.js';
|
|
27
|
+
import type { Logger } from '../../../src/logging/Logger.js';
|
|
28
|
+
import { MemoryLogger } from '../../../src/logging/MemoryLogger.js';
|
|
29
|
+
|
|
30
|
+
export class FakeNativeWindowService implements NativeWindowService {
|
|
31
|
+
windows: NativeWindowInfo[] = [];
|
|
32
|
+
focused: WindowMatcher[] = [];
|
|
33
|
+
minimized: WindowMatcher[] = [];
|
|
34
|
+
restored: WindowMatcher[] = [];
|
|
35
|
+
fullscreenCalls: Array<{ matcher: WindowMatcher; fullscreen: boolean }> = [];
|
|
36
|
+
|
|
37
|
+
async list(): Promise<NativeWindowInfo[]> { return this.windows; }
|
|
38
|
+
async find(matcher: WindowMatcher, _options?: WaitOptions): Promise<NativeWindowInfo | null> {
|
|
39
|
+
return this.windows.find(window => matcher.title === undefined || window.title === matcher.title) ?? null;
|
|
40
|
+
}
|
|
41
|
+
async waitFor(matcher: WindowMatcher, options?: WaitOptions): Promise<NativeWindowInfo> {
|
|
42
|
+
const found = await this.find(matcher, options);
|
|
43
|
+
if (found === null) throw new Error('Missing fake native window.');
|
|
44
|
+
return found;
|
|
45
|
+
}
|
|
46
|
+
async isOpen(matcher: WindowMatcher): Promise<boolean> { return (await this.find(matcher)) !== null; }
|
|
47
|
+
async focus(matcher: WindowMatcher): Promise<NativeWindowInfo> { this.focused.push(matcher); return this.waitFor(matcher); }
|
|
48
|
+
async ensureInView(matcher: WindowMatcher): Promise<NativeWindowInfo> { this.focused.push(matcher); return this.waitFor(matcher); }
|
|
49
|
+
async setFullscreen(matcher: WindowMatcher, fullscreen: boolean, _options?: WindowButtonOptions): Promise<void> { this.fullscreenCalls.push({ matcher, fullscreen }); }
|
|
50
|
+
async toggleFullscreen(matcher: WindowMatcher, _options?: WindowButtonOptions): Promise<void> { this.fullscreenCalls.push({ matcher, fullscreen: true }); }
|
|
51
|
+
async minimize(matcher: WindowMatcher): Promise<void> { this.minimized.push(matcher); }
|
|
52
|
+
async restore(matcher: WindowMatcher): Promise<void> { this.restored.push(matcher); }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class FakeNativeFileDialogService implements NativeFileDialogService {
|
|
56
|
+
selected: Array<{ filePath: string; options?: SelectFileOptions }> = [];
|
|
57
|
+
async selectFile(filePath: string, options?: SelectFileOptions): Promise<void> {
|
|
58
|
+
const record: { filePath: string; options?: SelectFileOptions } = { filePath };
|
|
59
|
+
if (options !== undefined) record.options = options;
|
|
60
|
+
this.selected.push(record);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class FakeNativeMouse implements NativeMouse {
|
|
65
|
+
moves: Array<{ point: ScreenPoint; options?: NativeMouseMoveOptions }> = [];
|
|
66
|
+
clicks: Array<{ point: ScreenPoint; options?: NativeMouseClickOptions }> = [];
|
|
67
|
+
regionClicks: Array<{ region: NativeRegion; options?: NativeMouseClickOptions }> = [];
|
|
68
|
+
browserClicks: Array<{ page: PageLike; point: BrowserPoint; options?: NativeMouseClickOptions }> = [];
|
|
69
|
+
async moveTo(point: ScreenPoint, options?: NativeMouseMoveOptions): Promise<void> { this.moves.push(options === undefined ? { point } : { point, options }); }
|
|
70
|
+
async click(point: ScreenPoint, options?: NativeMouseClickOptions): Promise<void> { this.clicks.push(options === undefined ? { point } : { point, options }); }
|
|
71
|
+
async clickRegion(region: NativeRegion, options?: NativeMouseClickOptions): Promise<void> { this.regionClicks.push(options === undefined ? { region } : { region, options }); }
|
|
72
|
+
async clickBrowserPoint(page: PageLike, point: BrowserPoint, options?: NativeMouseClickOptions): Promise<void> { this.browserClicks.push(options === undefined ? { page, point } : { page, point, options }); }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class FakeNativeKeyboard implements NativeKeyboard {
|
|
76
|
+
typed: string[] = [];
|
|
77
|
+
pressed: string[] = [];
|
|
78
|
+
shortcuts: Array<readonly string[]> = [];
|
|
79
|
+
async type(text: string): Promise<void> { this.typed.push(text); }
|
|
80
|
+
async pressKey(key: string): Promise<void> { this.pressed.push(key); }
|
|
81
|
+
async pressShortcut(keys: readonly string[]): Promise<void> { this.shortcuts.push(keys); }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class FakeNativeImageFinder implements NativeImageFinder {
|
|
85
|
+
matches = new Map<string, NativeImageMatch | null>();
|
|
86
|
+
async findTemplate(template: string | NativeTemplateRef, _options?: NativeImageSearchOptions): Promise<NativeImageMatch | null> {
|
|
87
|
+
return this.matches.get(typeof template === 'string' ? template : template.name) ?? null;
|
|
88
|
+
}
|
|
89
|
+
async waitForTemplate(template: string | NativeTemplateRef, options?: NativeImageSearchOptions): Promise<NativeImageMatch> {
|
|
90
|
+
const match = await this.findTemplate(template, options);
|
|
91
|
+
if (match === null) throw new Error('Missing fake image match.');
|
|
92
|
+
return match;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class FakeNativeCoordinateMapper {
|
|
97
|
+
transform: CoordinateTransform = {
|
|
98
|
+
offsetX: 0,
|
|
99
|
+
offsetY: 0,
|
|
100
|
+
scaleX: 1,
|
|
101
|
+
scaleY: 1,
|
|
102
|
+
calibratedAt: Date.now(),
|
|
103
|
+
viewport: { innerWidth: 1000, innerHeight: 800, devicePixelRatio: 1 }
|
|
104
|
+
};
|
|
105
|
+
async calibrate(_page: PageLike, _options?: CalibrationOptions): Promise<CoordinateTransform> { return this.transform; }
|
|
106
|
+
async browserToScreen(point: BrowserPoint, _options?: CoordinateMapOptions): Promise<ScreenPoint> {
|
|
107
|
+
return { x: this.transform.offsetX + point.x * this.transform.scaleX, y: this.transform.offsetY + point.y * this.transform.scaleY };
|
|
108
|
+
}
|
|
109
|
+
async screenToBrowser(point: ScreenPoint, _options?: CoordinateMapOptions): Promise<BrowserPoint> {
|
|
110
|
+
return { x: (point.x - this.transform.offsetX) / this.transform.scaleX, y: (point.y - this.transform.offsetY) / this.transform.scaleY };
|
|
111
|
+
}
|
|
112
|
+
invalidate(): void {}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface FakeNativeAutomationParts extends NativeAutomation {
|
|
116
|
+
windows: FakeNativeWindowService;
|
|
117
|
+
fileDialogs: FakeNativeFileDialogService;
|
|
118
|
+
mouse: FakeNativeMouse;
|
|
119
|
+
keyboard: FakeNativeKeyboard;
|
|
120
|
+
images: FakeNativeImageFinder;
|
|
121
|
+
coordinates: FakeNativeCoordinateMapper;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function createFakeNativeAutomation(logger: Logger = new MemoryLogger()): FakeNativeAutomationParts {
|
|
125
|
+
const automation = {
|
|
126
|
+
windows: new FakeNativeWindowService(),
|
|
127
|
+
fileDialogs: new FakeNativeFileDialogService(),
|
|
128
|
+
mouse: new FakeNativeMouse(),
|
|
129
|
+
keyboard: new FakeNativeKeyboard(),
|
|
130
|
+
images: new FakeNativeImageFinder(),
|
|
131
|
+
coordinates: new FakeNativeCoordinateMapper(),
|
|
132
|
+
actions: undefined as unknown as DefaultNativeActionRegistry
|
|
133
|
+
} satisfies FakeNativeAutomationParts;
|
|
134
|
+
|
|
135
|
+
const registry = new DefaultNativeActionRegistry(() => ({ automation, logger }));
|
|
136
|
+
automation.actions = registerDefaultNativeActions(registry, automation, logger);
|
|
137
|
+
return automation;
|
|
138
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { normalizeRuntimeConfig } from '../../../src/browser/RuntimeConfig.js';
|
|
6
|
+
|
|
7
|
+
function createUserDataDir(): string {
|
|
8
|
+
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'paf-native-config-'));
|
|
9
|
+
fs.mkdirSync(path.join(userDataDir, 'Default'));
|
|
10
|
+
return userDataDir;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('native RuntimeConfig normalization', () => {
|
|
14
|
+
it('preserves backward compatibility when native config is omitted', () => {
|
|
15
|
+
const config = normalizeRuntimeConfig({
|
|
16
|
+
browser: {
|
|
17
|
+
mode: 'existing-profile',
|
|
18
|
+
userDataDir: createUserDataDir(),
|
|
19
|
+
profileDirectory: 'Default',
|
|
20
|
+
headless: false
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(config.native).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('normalizes native macOS automation defaults when opted in', () => {
|
|
28
|
+
const config = normalizeRuntimeConfig({
|
|
29
|
+
browser: {
|
|
30
|
+
mode: 'existing-profile',
|
|
31
|
+
userDataDir: createUserDataDir(),
|
|
32
|
+
profileDirectory: 'Default',
|
|
33
|
+
headless: false
|
|
34
|
+
},
|
|
35
|
+
native: {
|
|
36
|
+
templatesDir: '/tmp/templates'
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(config.native).toMatchObject({
|
|
41
|
+
enabled: true,
|
|
42
|
+
platform: 'macos',
|
|
43
|
+
templatesDir: '/tmp/templates',
|
|
44
|
+
imageConfidence: 0.9,
|
|
45
|
+
mouse: {
|
|
46
|
+
jitterPixels: 3,
|
|
47
|
+
minJitterPixels: 2
|
|
48
|
+
},
|
|
49
|
+
macos: {
|
|
50
|
+
targetApplicationName: 'Google Chrome',
|
|
51
|
+
osascriptTimeoutMs: 15_000,
|
|
52
|
+
preferAppleScriptForWindows: true,
|
|
53
|
+
preferVisualTemplatesForDialogs: false
|
|
54
|
+
},
|
|
55
|
+
coordinateCalibration: {
|
|
56
|
+
enabled: true,
|
|
57
|
+
markerSizePx: 22,
|
|
58
|
+
timeoutMs: 15_000,
|
|
59
|
+
cacheTtlMs: 60_000
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { BrowserFactory } from '../../../src/browser/BrowserFactory.js';
|
|
6
|
+
import { BrowserSession } from '../../../src/browser/BrowserSession.js';
|
|
7
|
+
import type { RuntimeConfig } from '../../../src/browser/RuntimeConfig.js';
|
|
8
|
+
import { defineActor } from '../../../src/core/defineActor.js';
|
|
9
|
+
import { ActorRunner } from '../../../src/core/ActorRunner.js';
|
|
10
|
+
import { PuppeteerPageAdapter } from '../../../src/interaction/PageAdapter.js';
|
|
11
|
+
import { MemoryLogger } from '../../../src/logging/MemoryLogger.js';
|
|
12
|
+
import type { NativeAutomation } from '../../../src/native/types.js';
|
|
13
|
+
import { FakeHumanInteractor } from '../../fixtures/FakeCursor.js';
|
|
14
|
+
import { FakeBrowser, FakePage } from '../../fixtures/FakePage.js';
|
|
15
|
+
import { createFakeNativeAutomation } from '../../fixtures/native/FakeNativeAutomation.js';
|
|
16
|
+
|
|
17
|
+
class StaticBrowserFactory extends BrowserFactory {
|
|
18
|
+
constructor(private readonly session: BrowserSession) {
|
|
19
|
+
super();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override async launch(): Promise<BrowserSession> {
|
|
23
|
+
return this.session;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createConfig(): RuntimeConfig {
|
|
28
|
+
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'paf-native-runner-'));
|
|
29
|
+
fs.mkdirSync(path.join(userDataDir, 'Default'));
|
|
30
|
+
return {
|
|
31
|
+
browser: {
|
|
32
|
+
mode: 'existing-profile',
|
|
33
|
+
userDataDir,
|
|
34
|
+
profileDirectory: 'Default',
|
|
35
|
+
headless: false
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('ActorRunner native automation integration', () => {
|
|
41
|
+
it('injects context.native from the configured nativeAutomationFactory', async () => {
|
|
42
|
+
const config = createConfig();
|
|
43
|
+
const page = new FakePage();
|
|
44
|
+
const browser = new FakeBrowser([page]);
|
|
45
|
+
const session = new BrowserSession(browser, browser.context, page, config.browser);
|
|
46
|
+
const native = createFakeNativeAutomation();
|
|
47
|
+
native.windows.windows = [{ title: 'Chrome', appName: 'Google Chrome', region: { x: 0, y: 0, width: 100, height: 100 } }];
|
|
48
|
+
|
|
49
|
+
const runner = new ActorRunner({
|
|
50
|
+
config,
|
|
51
|
+
browserFactory: new StaticBrowserFactory(session),
|
|
52
|
+
loginFlow: { ensureAuthenticated: async () => undefined } as never,
|
|
53
|
+
logger: new MemoryLogger(),
|
|
54
|
+
interactorFactory: () => new FakeHumanInteractor(),
|
|
55
|
+
pageAdapterFactory: pageLike => new PuppeteerPageAdapter(pageLike),
|
|
56
|
+
nativeAutomationFactory: () => native as NativeAutomation
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const actor = defineActor({
|
|
60
|
+
id: 'example',
|
|
61
|
+
baseUrl: 'https://example.com',
|
|
62
|
+
tasks: {
|
|
63
|
+
checkWindow: async context => context.native.windows.isOpen({ title: 'Chrome' })
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await expect(runner.run(actor, 'checkWindow')).resolves.toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { AppleScriptExecutor } from '../../../src/native/drivers.js';
|
|
3
|
+
import { MacOSAppleScriptClient } from '../../../src/native/macos/MacOSAppleScriptClient.js';
|
|
4
|
+
|
|
5
|
+
class FakeExecutor implements AppleScriptExecutor {
|
|
6
|
+
scripts: string[] = [];
|
|
7
|
+
result = '';
|
|
8
|
+
async execute(script: string): Promise<string> {
|
|
9
|
+
this.scripts.push(script);
|
|
10
|
+
return this.result;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('MacOSAppleScriptClient', () => {
|
|
15
|
+
it('escapes app names, titles, and file paths in generated scripts', async () => {
|
|
16
|
+
const executor = new FakeExecutor();
|
|
17
|
+
const client = new MacOSAppleScriptClient({ executor });
|
|
18
|
+
|
|
19
|
+
await client.setFullscreen({ appName: 'Google "Chrome"', title: 'A "quoted" window' }, true);
|
|
20
|
+
await client.selectFileInOpenDialog('/Users/me/A "quoted" file.pdf', 'Google "Chrome"');
|
|
21
|
+
|
|
22
|
+
expect(executor.scripts[0]).toContain('process "Google \\"Chrome\\""');
|
|
23
|
+
expect(executor.scripts[0]).toContain('"A \\"quoted\\" window"');
|
|
24
|
+
expect(executor.scripts[1]).toContain('set targetPath to "/Users/me/A \\"quoted\\" file.pdf"');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('parses boolean window-open output', async () => {
|
|
28
|
+
const executor = new FakeExecutor();
|
|
29
|
+
executor.result = 'true';
|
|
30
|
+
const client = new MacOSAppleScriptClient({ executor });
|
|
31
|
+
|
|
32
|
+
await expect(client.isWindowOpen({ appName: 'Google Chrome', titleIncludes: 'Upwork' })).resolves.toBe(true);
|
|
33
|
+
expect(executor.scripts[0]).toContain('name of candidateWindow contains "Upwork"');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DefaultNativeActionRegistry, defineNativeAction } from '../../../src/native/NativeActionRegistry.js';
|
|
3
|
+
import { NativeAutomationError } from '../../../src/native/errors.js';
|
|
4
|
+
import { MemoryLogger } from '../../../src/logging/MemoryLogger.js';
|
|
5
|
+
import { createFakeNativeAutomation } from '../../fixtures/native/FakeNativeAutomation.js';
|
|
6
|
+
|
|
7
|
+
describe('DefaultNativeActionRegistry', () => {
|
|
8
|
+
it('registers, lists, and runs typed native actions', async () => {
|
|
9
|
+
const logger = new MemoryLogger();
|
|
10
|
+
const automation = createFakeNativeAutomation(logger);
|
|
11
|
+
const registry = new DefaultNativeActionRegistry(() => ({ automation, logger }));
|
|
12
|
+
|
|
13
|
+
registry.register(defineNativeAction<{ value: number }, number>({
|
|
14
|
+
name: 'math.double',
|
|
15
|
+
description: 'Double a number.',
|
|
16
|
+
run: async (_context, input) => input.value * 2
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
expect(registry.list()).toEqual([{ name: 'math.double', description: 'Double a number.' }]);
|
|
20
|
+
await expect(registry.run('math.double', { value: 21 })).resolves.toBe(42);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('rejects duplicate and missing actions clearly', () => {
|
|
24
|
+
const logger = new MemoryLogger();
|
|
25
|
+
const automation = createFakeNativeAutomation(logger);
|
|
26
|
+
const registry = new DefaultNativeActionRegistry(() => ({ automation, logger }));
|
|
27
|
+
const action = defineNativeAction({ name: 'x', run: async () => undefined });
|
|
28
|
+
|
|
29
|
+
registry.register(action);
|
|
30
|
+
|
|
31
|
+
expect(() => registry.register(action)).toThrow(NativeAutomationError);
|
|
32
|
+
expect(() => registry.get('missing')).toThrow(NativeAutomationError);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { HTTPResponse, WaitForOptions, WaitForSelectorOptions } from 'puppeteer-core';
|
|
3
|
+
import { ImageBasedCoordinateMapper } from '../../../src/native/NativeCoordinateMapper.js';
|
|
4
|
+
import { CoordinateCalibrationError } from '../../../src/native/errors.js';
|
|
5
|
+
import type { BrowserViewportSnapshot, NativeImageFinder, NativeImageMatch, NativeImageSearchOptions, NativeTemplateRef } from '../../../src/native/types.js';
|
|
6
|
+
import type { KeyboardLike, PageLike } from '../../../src/browser/PuppeteerLike.js';
|
|
7
|
+
|
|
8
|
+
class CalibrationPage implements PageLike {
|
|
9
|
+
keyboard: KeyboardLike = { type: async () => undefined, press: async () => undefined };
|
|
10
|
+
evaluates: string[] = [];
|
|
11
|
+
screenshots: Record<string, unknown>[] = [];
|
|
12
|
+
removedMarkers = 0;
|
|
13
|
+
viewport: BrowserViewportSnapshot = {
|
|
14
|
+
screenX: 0,
|
|
15
|
+
screenY: 0,
|
|
16
|
+
outerWidth: 1000,
|
|
17
|
+
outerHeight: 900,
|
|
18
|
+
innerWidth: 800,
|
|
19
|
+
innerHeight: 600,
|
|
20
|
+
devicePixelRatio: 1
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
async goto(_url: string, _options?: WaitForOptions): Promise<HTTPResponse | null> { return null; }
|
|
24
|
+
async waitForSelector(_selector: string, _options?: WaitForSelectorOptions): Promise<unknown> { return {}; }
|
|
25
|
+
async waitForNavigation(_options?: WaitForOptions): Promise<HTTPResponse | null> { return null; }
|
|
26
|
+
async click(_selector: string): Promise<void> {}
|
|
27
|
+
async focus(_selector: string): Promise<void> {}
|
|
28
|
+
async $eval<T>(_selector: string, _pageFunction: (element: Element, ...args: unknown[]) => T, ..._args: unknown[]): Promise<T> { throw new Error('not used'); }
|
|
29
|
+
async $$eval<T>(_selector: string, _pageFunction: (elements: Element[], ...args: unknown[]) => T, ..._args: unknown[]): Promise<T> { throw new Error('not used'); }
|
|
30
|
+
async evaluate<T>(pageFunction: (...args: unknown[]) => T, ...args: unknown[]): Promise<T> {
|
|
31
|
+
const source = pageFunction.toString();
|
|
32
|
+
this.evaluates.push(source);
|
|
33
|
+
if (source.includes('screenX')) return this.viewport as T;
|
|
34
|
+
if (args.length === 0 && source.includes('__paf_native_coordinate_markers__') && source.includes('remove')) this.removedMarkers += 1;
|
|
35
|
+
return undefined as T;
|
|
36
|
+
}
|
|
37
|
+
url(): string { return 'https://example.com'; }
|
|
38
|
+
async title(): Promise<string> { return 'Example'; }
|
|
39
|
+
async content(): Promise<string> { return '<html></html>'; }
|
|
40
|
+
async screenshot(options?: Record<string, unknown>): Promise<Uint8Array> {
|
|
41
|
+
this.screenshots.push(options ?? {});
|
|
42
|
+
return new Uint8Array([1, 2, 3]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class StaticImageFinder implements NativeImageFinder {
|
|
47
|
+
constructor(private readonly matches: Record<string, NativeImageMatch>) {}
|
|
48
|
+
async findTemplate(template: string | NativeTemplateRef, _options?: NativeImageSearchOptions): Promise<NativeImageMatch | null> {
|
|
49
|
+
return this.matches[typeof template === 'string' ? template : template.name] ?? null;
|
|
50
|
+
}
|
|
51
|
+
async waitForTemplate(template: string | NativeTemplateRef, options?: NativeImageSearchOptions): Promise<NativeImageMatch> {
|
|
52
|
+
const match = await this.findTemplate(template, options);
|
|
53
|
+
if (match === null) throw new Error('missing match');
|
|
54
|
+
return match;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('ImageBasedCoordinateMapper', () => {
|
|
59
|
+
it('calibrates offset and scale from two screen marker matches', async () => {
|
|
60
|
+
const page = new CalibrationPage();
|
|
61
|
+
const mapper = new ImageBasedCoordinateMapper({
|
|
62
|
+
images: new StaticImageFinder({
|
|
63
|
+
'paf-native-calibration-a.png': { region: { x: 211, y: 321, width: 22, height: 22 } },
|
|
64
|
+
'paf-native-calibration-b.png': { region: { x: 511, y: 521, width: 22, height: 22 } }
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const transform = await mapper.calibrate(page);
|
|
69
|
+
|
|
70
|
+
expect(transform.scaleX).toBe(1);
|
|
71
|
+
expect(transform.scaleY).toBe(1);
|
|
72
|
+
expect(transform.offsetX).toBe(91);
|
|
73
|
+
expect(transform.offsetY).toBe(201);
|
|
74
|
+
await expect(mapper.browserToScreen({ x: 10, y: 20 })).resolves.toEqual({ x: 101, y: 221 });
|
|
75
|
+
await expect(mapper.screenToBrowser({ x: 101, y: 221 })).resolves.toEqual({ x: 10, y: 20 });
|
|
76
|
+
expect(page.screenshots).toHaveLength(2);
|
|
77
|
+
expect(page.removedMarkers).toBe(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('removes markers and rejects implausible scale values', async () => {
|
|
81
|
+
const page = new CalibrationPage();
|
|
82
|
+
const mapper = new ImageBasedCoordinateMapper({
|
|
83
|
+
images: new StaticImageFinder({
|
|
84
|
+
'paf-native-calibration-a.png': { region: { x: 0, y: 0, width: 22, height: 22 } },
|
|
85
|
+
'paf-native-calibration-b.png': { region: { x: 2000, y: 2000, width: 22, height: 22 } }
|
|
86
|
+
})
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await expect(mapper.calibrate(page)).rejects.toBeInstanceOf(CoordinateCalibrationError);
|
|
90
|
+
expect(page.removedMarkers).toBe(1);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { DefaultNativeFileDialogService, type FileDialogAccessibilityStrategy } from '../../../src/native/NativeFileDialogService.js';
|
|
6
|
+
import { NativeFileDialogError } from '../../../src/native/errors.js';
|
|
7
|
+
import type { NativeKeyboard, SelectFileOptions } from '../../../src/native/types.js';
|
|
8
|
+
import { FakeNativeImageFinder, FakeNativeMouse } from '../../fixtures/native/FakeNativeAutomation.js';
|
|
9
|
+
|
|
10
|
+
class FakeKeyboard implements NativeKeyboard {
|
|
11
|
+
typed: string[] = [];
|
|
12
|
+
pressed: string[] = [];
|
|
13
|
+
shortcuts: Array<readonly string[]> = [];
|
|
14
|
+
async type(text: string): Promise<void> { this.typed.push(text); }
|
|
15
|
+
async pressKey(key: string): Promise<void> { this.pressed.push(key); }
|
|
16
|
+
async pressShortcut(keys: readonly string[]): Promise<void> { this.shortcuts.push([...keys]); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class FakeAccessibility implements FileDialogAccessibilityStrategy {
|
|
20
|
+
calls: Array<{ filePath: string; options: SelectFileOptions }> = [];
|
|
21
|
+
constructor(private readonly error?: Error) {}
|
|
22
|
+
async selectFile(filePath: string, options: SelectFileOptions): Promise<void> {
|
|
23
|
+
this.calls.push({ filePath, options });
|
|
24
|
+
if (this.error !== undefined) throw this.error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function createTempFile(): Promise<string> {
|
|
29
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'paf-dialog-'));
|
|
30
|
+
const filePath = path.join(dir, 'résumé "quoted".pdf');
|
|
31
|
+
await fs.writeFile(filePath, 'test');
|
|
32
|
+
return filePath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('DefaultNativeFileDialogService', () => {
|
|
36
|
+
it('validates that the selected file exists', async () => {
|
|
37
|
+
const service = new DefaultNativeFileDialogService({ keyboard: new FakeKeyboard() });
|
|
38
|
+
|
|
39
|
+
await expect(service.selectFile('/definitely/missing/file.pdf')).rejects.toBeInstanceOf(NativeFileDialogError);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('uses the AppleScript/accessibility strategy first in auto mode', async () => {
|
|
43
|
+
const filePath = await createTempFile();
|
|
44
|
+
const accessibility = new FakeAccessibility();
|
|
45
|
+
const keyboard = new FakeKeyboard();
|
|
46
|
+
const service = new DefaultNativeFileDialogService({ keyboard, accessibility });
|
|
47
|
+
|
|
48
|
+
await service.selectFile(filePath, { appName: 'Google Chrome' });
|
|
49
|
+
|
|
50
|
+
expect(accessibility.calls).toEqual([{ filePath, options: { appName: 'Google Chrome' } }]);
|
|
51
|
+
expect(keyboard.typed).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('falls back to keyboard selection when accessibility fails in auto mode', async () => {
|
|
55
|
+
const filePath = await createTempFile();
|
|
56
|
+
const keyboard = new FakeKeyboard();
|
|
57
|
+
const service = new DefaultNativeFileDialogService({
|
|
58
|
+
keyboard,
|
|
59
|
+
accessibility: new FakeAccessibility(new Error('permission denied'))
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await service.selectFile(filePath);
|
|
63
|
+
|
|
64
|
+
expect(keyboard.shortcuts).toEqual([['Meta', 'Shift', 'G']]);
|
|
65
|
+
expect(keyboard.typed).toEqual([filePath]);
|
|
66
|
+
expect(keyboard.pressed).toEqual(['Enter', 'Enter']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('can click visual templates for input and open button', async () => {
|
|
70
|
+
const filePath = await createTempFile();
|
|
71
|
+
const keyboard = new FakeKeyboard();
|
|
72
|
+
const images = new FakeNativeImageFinder();
|
|
73
|
+
const mouse = new FakeNativeMouse();
|
|
74
|
+
images.matches.set('input.png', { region: { x: 10, y: 20, width: 100, height: 20 } });
|
|
75
|
+
images.matches.set('open.png', { region: { x: 200, y: 300, width: 80, height: 30 } });
|
|
76
|
+
const service = new DefaultNativeFileDialogService({ keyboard, images, mouse });
|
|
77
|
+
|
|
78
|
+
await service.selectFile(filePath, {
|
|
79
|
+
strategy: 'visual',
|
|
80
|
+
inputFieldTemplate: 'input.png',
|
|
81
|
+
openButtonTemplate: 'open.png'
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(mouse.clicks.map(click => click.point)).toEqual([
|
|
85
|
+
{ x: 60, y: 30 },
|
|
86
|
+
{ x: 240, y: 315 }
|
|
87
|
+
]);
|
|
88
|
+
expect(keyboard.typed).toEqual([filePath]);
|
|
89
|
+
expect(keyboard.pressed).toEqual(['Enter']);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { HumanizedNativeMouse } from '../../../src/native/NativeMouse.js';
|
|
3
|
+
import type { NativeMouseButton, NativePoint } from '../../../src/native/types.js';
|
|
4
|
+
import type { NativeMouseDriver } from '../../../src/native/drivers.js';
|
|
5
|
+
|
|
6
|
+
class FakeMouseDriver implements NativeMouseDriver {
|
|
7
|
+
position: NativePoint = { x: 0, y: 0 };
|
|
8
|
+
moves: NativePoint[] = [];
|
|
9
|
+
clicks: NativeMouseButton[] = [];
|
|
10
|
+
|
|
11
|
+
async getPosition(): Promise<NativePoint> {
|
|
12
|
+
return this.position;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async move(point: NativePoint): Promise<void> {
|
|
16
|
+
this.position = point;
|
|
17
|
+
this.moves.push(point);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async click(button: NativeMouseButton): Promise<void> {
|
|
21
|
+
this.clicks.push(button);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('HumanizedNativeMouse', () => {
|
|
26
|
+
it('moves through a ghost-cursor-style path and applies default 2-3px jitter', async () => {
|
|
27
|
+
const driver = new FakeMouseDriver();
|
|
28
|
+
const pathCalls: Array<{ from: NativePoint; to: NativePoint }> = [];
|
|
29
|
+
const mouse = new HumanizedNativeMouse(driver, {
|
|
30
|
+
coordinateMapper: { browserToScreen: async point => point },
|
|
31
|
+
random: () => 0,
|
|
32
|
+
pathGenerator: (from, to) => {
|
|
33
|
+
pathCalls.push({ from, to });
|
|
34
|
+
return [from, { x: 5, y: 5 }, to];
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await mouse.moveTo({ x: 10, y: 10 });
|
|
39
|
+
|
|
40
|
+
expect(pathCalls).toEqual([{ from: { x: 0, y: 0 }, to: { x: 12, y: 10 } }]);
|
|
41
|
+
expect(driver.moves).toEqual([
|
|
42
|
+
{ x: 2, y: 0 },
|
|
43
|
+
{ x: 7, y: 5 },
|
|
44
|
+
{ x: 12, y: 10 }
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('clicks by moving first, then clicking the requested button', async () => {
|
|
49
|
+
const driver = new FakeMouseDriver();
|
|
50
|
+
const mouse = new HumanizedNativeMouse(driver, {
|
|
51
|
+
coordinateMapper: { browserToScreen: async point => point },
|
|
52
|
+
random: () => 0.5,
|
|
53
|
+
pathGenerator: (_from, to) => [to]
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await mouse.click({ x: 20, y: 30 }, { button: 'right', precise: true });
|
|
57
|
+
|
|
58
|
+
expect(driver.moves).toEqual([{ x: 20, y: 30 }]);
|
|
59
|
+
expect(driver.clicks).toEqual(['right']);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('clamps jittered region clicks inside small regions', async () => {
|
|
63
|
+
const driver = new FakeMouseDriver();
|
|
64
|
+
const mouse = new HumanizedNativeMouse(driver, {
|
|
65
|
+
coordinateMapper: { browserToScreen: async point => point },
|
|
66
|
+
random: () => 0,
|
|
67
|
+
pathGenerator: (_from, to) => [to]
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await mouse.clickRegion({ x: 10, y: 10, width: 2, height: 2 }, { regionPaddingPx: 0 });
|
|
71
|
+
|
|
72
|
+
const finalMove = driver.moves.at(-1);
|
|
73
|
+
expect(finalMove?.x).toBeGreaterThanOrEqual(10);
|
|
74
|
+
expect(finalMove?.x).toBeLessThanOrEqual(12);
|
|
75
|
+
expect(finalMove?.y).toBeGreaterThanOrEqual(10);
|
|
76
|
+
expect(finalMove?.y).toBeLessThanOrEqual(12);
|
|
77
|
+
expect(driver.clicks).toEqual(['left']);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('maps browser viewport points before native clicking', async () => {
|
|
81
|
+
const driver = new FakeMouseDriver();
|
|
82
|
+
const mouse = new HumanizedNativeMouse(driver, {
|
|
83
|
+
coordinateMapper: { browserToScreen: async point => ({ x: point.x + 100, y: point.y + 200 }) },
|
|
84
|
+
pathGenerator: (_from, to) => [to]
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await mouse.clickBrowserPoint({} as never, { x: 5, y: 7 }, { precise: true });
|
|
88
|
+
|
|
89
|
+
expect(driver.moves).toEqual([{ x: 105, y: 207 }]);
|
|
90
|
+
});
|
|
91
|
+
});
|