@epsilon-asi/actors 0.0.22 → 0.0.32
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 +30 -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/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 +46 -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 fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { Logger } from '../logging/Logger.js';
|
|
4
|
+
import { NativeFileDialogError } from './errors.js';
|
|
5
|
+
import type { NativeImageFinder, NativeKeyboard, NativeMouse, SelectFileOptions } from './types.js';
|
|
6
|
+
import { centerOf } from './utils/geometry.js';
|
|
7
|
+
import { redactPath } from './utils/redactNative.js';
|
|
8
|
+
|
|
9
|
+
export interface FileDialogAccessibilityStrategy {
|
|
10
|
+
selectFile(filePath: string, options: SelectFileOptions): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface NativeFileDialogServiceDependencies {
|
|
14
|
+
keyboard: NativeKeyboard;
|
|
15
|
+
mouse?: NativeMouse;
|
|
16
|
+
images?: NativeImageFinder;
|
|
17
|
+
accessibility?: FileDialogAccessibilityStrategy;
|
|
18
|
+
logger?: Logger;
|
|
19
|
+
defaultTimeoutMs?: number;
|
|
20
|
+
revealFilePathsInLogs?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class DefaultNativeFileDialogService {
|
|
24
|
+
constructor(private readonly deps: NativeFileDialogServiceDependencies) {}
|
|
25
|
+
|
|
26
|
+
async selectFile(filePath: string, options: SelectFileOptions = {}): Promise<void> {
|
|
27
|
+
const absolutePath = path.resolve(filePath);
|
|
28
|
+
await this.assertFileExists(absolutePath);
|
|
29
|
+
|
|
30
|
+
const strategy = options.strategy ?? 'auto';
|
|
31
|
+
const failures: unknown[] = [];
|
|
32
|
+
|
|
33
|
+
if (strategy === 'applescript' || strategy === 'auto') {
|
|
34
|
+
if (this.deps.accessibility !== undefined) {
|
|
35
|
+
try {
|
|
36
|
+
await this.deps.accessibility.selectFile(absolutePath, options);
|
|
37
|
+
this.logSuccess('applescript', absolutePath);
|
|
38
|
+
return;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
failures.push(error);
|
|
41
|
+
if (strategy === 'applescript') throw this.wrapFailure('applescript', absolutePath, failures);
|
|
42
|
+
}
|
|
43
|
+
} else if (strategy === 'applescript') {
|
|
44
|
+
throw new NativeFileDialogError('AppleScript file-dialog strategy is not available.', { strategy });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (strategy === 'visual' || strategy === 'auto') {
|
|
49
|
+
if (this.deps.images !== undefined && this.deps.mouse !== undefined) {
|
|
50
|
+
try {
|
|
51
|
+
await this.selectFileVisually(absolutePath, options);
|
|
52
|
+
this.logSuccess('visual', absolutePath);
|
|
53
|
+
return;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
failures.push(error);
|
|
56
|
+
if (strategy === 'visual') throw this.wrapFailure('visual', absolutePath, failures);
|
|
57
|
+
}
|
|
58
|
+
} else if (strategy === 'visual') {
|
|
59
|
+
throw new NativeFileDialogError('Visual file-dialog strategy requires image finder and native mouse dependencies.', { strategy });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (strategy === 'keyboard' || strategy === 'auto') {
|
|
64
|
+
try {
|
|
65
|
+
await this.selectFileWithKeyboard(absolutePath, options);
|
|
66
|
+
this.logSuccess('keyboard', absolutePath);
|
|
67
|
+
return;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
failures.push(error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
throw this.wrapFailure(strategy, absolutePath, failures);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async assertFileExists(filePath: string): Promise<void> {
|
|
77
|
+
const stat = await fs.stat(filePath).catch(() => null);
|
|
78
|
+
if (stat === null || !stat.isFile()) {
|
|
79
|
+
throw new NativeFileDialogError('Cannot select file because the path does not exist or is not a file.', {
|
|
80
|
+
filePath: redactPath(filePath, this.deps.revealFilePathsInLogs)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async selectFileVisually(filePath: string, options: SelectFileOptions): Promise<void> {
|
|
86
|
+
if (this.deps.images === undefined || this.deps.mouse === undefined) {
|
|
87
|
+
throw new NativeFileDialogError('Visual file-dialog strategy requires image finder and native mouse dependencies.');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (options.inputFieldTemplate !== undefined) {
|
|
91
|
+
const input = await this.deps.images.waitForTemplate(options.inputFieldTemplate, this.imageSearchOptions(options));
|
|
92
|
+
await this.deps.mouse.click(centerOf(input.region));
|
|
93
|
+
} else {
|
|
94
|
+
await this.deps.keyboard.pressShortcut(['Meta', 'Shift', 'G']);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await this.deps.keyboard.type(filePath);
|
|
98
|
+
await this.deps.keyboard.pressKey('Enter');
|
|
99
|
+
|
|
100
|
+
if ((options.clickOpen ?? true) && options.openButtonTemplate !== undefined) {
|
|
101
|
+
const openButton = await this.deps.images.waitForTemplate(options.openButtonTemplate, this.imageSearchOptions(options));
|
|
102
|
+
await this.deps.mouse.click(centerOf(openButton.region));
|
|
103
|
+
} else if (options.clickOpen ?? true) {
|
|
104
|
+
await this.deps.keyboard.pressKey('Enter');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async selectFileWithKeyboard(filePath: string, options: SelectFileOptions): Promise<void> {
|
|
109
|
+
await this.deps.keyboard.pressShortcut(['Meta', 'Shift', 'G']);
|
|
110
|
+
await this.deps.keyboard.type(filePath);
|
|
111
|
+
await this.deps.keyboard.pressKey('Enter');
|
|
112
|
+
if (options.clickOpen ?? true) {
|
|
113
|
+
await this.deps.keyboard.pressKey('Enter');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private imageSearchOptions(options: SelectFileOptions) {
|
|
118
|
+
const searchOptions: { timeoutMs?: number; intervalMs?: number } = {};
|
|
119
|
+
if (options.timeoutMs !== undefined) searchOptions.timeoutMs = options.timeoutMs;
|
|
120
|
+
if (options.intervalMs !== undefined) searchOptions.intervalMs = options.intervalMs;
|
|
121
|
+
return searchOptions;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private wrapFailure(strategy: string, filePath: string, failures: unknown[]): NativeFileDialogError {
|
|
125
|
+
return new NativeFileDialogError('Failed to select a file in the native file dialog.', {
|
|
126
|
+
strategy,
|
|
127
|
+
filePath: redactPath(filePath, this.deps.revealFilePathsInLogs),
|
|
128
|
+
failures
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private logSuccess(strategy: string, filePath: string): void {
|
|
133
|
+
this.deps.logger?.debug('Selected file in native dialog.', {
|
|
134
|
+
strategy,
|
|
135
|
+
filePath: redactPath(filePath, this.deps.revealFilePathsInLogs)
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { NativeImageFinder, NativeImageMatch, NativeImageSearchOptions, NativeTemplateRef } from './types.js';
|
|
2
|
+
import { NativeImageNotFoundError } from './errors.js';
|
|
3
|
+
import { waitFor } from './utils/waitFor.js';
|
|
4
|
+
|
|
5
|
+
export interface NativeImageFinderDriver {
|
|
6
|
+
findTemplate(template: string | NativeTemplateRef, options?: NativeImageSearchOptions): Promise<NativeImageMatch | null>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class DefaultNativeImageFinder implements NativeImageFinder {
|
|
10
|
+
constructor(private readonly driver: NativeImageFinderDriver, private readonly defaultTimeoutMs = 15_000) {}
|
|
11
|
+
|
|
12
|
+
async findTemplate(template: string | NativeTemplateRef, options?: NativeImageSearchOptions): Promise<NativeImageMatch | null> {
|
|
13
|
+
return this.driver.findTemplate(template, options);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async waitForTemplate(template: string | NativeTemplateRef, options: NativeImageSearchOptions = {}): Promise<NativeImageMatch> {
|
|
17
|
+
try {
|
|
18
|
+
return await waitFor(
|
|
19
|
+
() => this.findTemplate(template, options),
|
|
20
|
+
{
|
|
21
|
+
timeoutMs: options.timeoutMs ?? this.defaultTimeoutMs,
|
|
22
|
+
intervalMs: options.intervalMs ?? 100,
|
|
23
|
+
description: `native image template ${typeof template === 'string' ? template : template.name}`
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new NativeImageNotFoundError({
|
|
28
|
+
template: typeof template === 'string' ? template : template.name,
|
|
29
|
+
cause: error
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { NativeKeyboardDriver } from './drivers.js';
|
|
2
|
+
import type { NativeKeyboard, NativeKeyboardTypeOptions } from './types.js';
|
|
3
|
+
|
|
4
|
+
export class DefaultNativeKeyboard implements NativeKeyboard {
|
|
5
|
+
constructor(private readonly driver: NativeKeyboardDriver) {}
|
|
6
|
+
|
|
7
|
+
async type(text: string, options: NativeKeyboardTypeOptions = {}): Promise<void> {
|
|
8
|
+
await this.driver.type(text, options.delayMs === undefined ? undefined : { delayMs: options.delayMs });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async pressKey(key: string): Promise<void> {
|
|
12
|
+
await this.driver.pressKey(key);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async pressShortcut(keys: readonly string[]): Promise<void> {
|
|
16
|
+
await this.driver.pressShortcut(keys);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { path as ghostCursorPath } from 'ghost-cursor';
|
|
2
|
+
import type { PageLike } from '../browser/PuppeteerLike.js';
|
|
3
|
+
import type { NativeMouseDriver } from './drivers.js';
|
|
4
|
+
import type {
|
|
5
|
+
BrowserPoint,
|
|
6
|
+
NativeMouse,
|
|
7
|
+
NativeMouseClickOptions,
|
|
8
|
+
NativeMouseMoveOptions,
|
|
9
|
+
NativePoint,
|
|
10
|
+
NativeRegion,
|
|
11
|
+
ScreenPoint
|
|
12
|
+
} from './types.js';
|
|
13
|
+
import { centerOf, clampPointToRegion, randomPointInRegion, roundPoint } from './utils/geometry.js';
|
|
14
|
+
|
|
15
|
+
export type NativeRandomFunction = () => number;
|
|
16
|
+
export type NativeSleepFunction = (ms: number) => Promise<void>;
|
|
17
|
+
export type PathGenerator = (from: NativePoint, to: NativePoint, options?: { moveSpeed?: number }) => NativePoint[];
|
|
18
|
+
|
|
19
|
+
export interface HumanizedNativeMouseOptions {
|
|
20
|
+
jitterPixels?: number;
|
|
21
|
+
minJitterPixels?: number;
|
|
22
|
+
moveSpeed?: number;
|
|
23
|
+
random?: NativeRandomFunction;
|
|
24
|
+
sleep?: NativeSleepFunction;
|
|
25
|
+
pathGenerator?: PathGenerator;
|
|
26
|
+
coordinateMapper: {
|
|
27
|
+
browserToScreen(point: BrowserPoint, options?: { page?: PageLike }): Promise<ScreenPoint>;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const defaultSleep: NativeSleepFunction = async ms => {
|
|
32
|
+
if (ms <= 0) return;
|
|
33
|
+
await new Promise<void>(resolve => setTimeout(resolve, ms));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function defaultPathGenerator(from: NativePoint, to: NativePoint, options?: { moveSpeed?: number }): NativePoint[] {
|
|
37
|
+
const pathOptions = options?.moveSpeed === undefined ? undefined : { moveSpeed: options.moveSpeed };
|
|
38
|
+
return ghostCursorPath(from, to, pathOptions) as NativePoint[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeJitter(minJitterPixels: number | undefined, jitterPixels: number | undefined): { min: number; max: number } {
|
|
42
|
+
const max = Math.max(0, jitterPixels ?? 3);
|
|
43
|
+
const min = Math.min(max, Math.max(0, minJitterPixels ?? Math.min(2, max)));
|
|
44
|
+
return { min, max };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class HumanizedNativeMouse implements NativeMouse {
|
|
48
|
+
private readonly random: NativeRandomFunction;
|
|
49
|
+
private readonly sleep: NativeSleepFunction;
|
|
50
|
+
private readonly pathGenerator: PathGenerator;
|
|
51
|
+
private readonly jitterPixels: number;
|
|
52
|
+
private readonly minJitterPixels: number;
|
|
53
|
+
private readonly moveSpeed: number | undefined;
|
|
54
|
+
|
|
55
|
+
constructor(private readonly driver: NativeMouseDriver, private readonly options: HumanizedNativeMouseOptions) {
|
|
56
|
+
this.random = options.random ?? Math.random;
|
|
57
|
+
this.sleep = options.sleep ?? defaultSleep;
|
|
58
|
+
this.pathGenerator = options.pathGenerator ?? defaultPathGenerator;
|
|
59
|
+
this.jitterPixels = options.jitterPixels ?? 3;
|
|
60
|
+
this.minJitterPixels = options.minJitterPixels ?? 2;
|
|
61
|
+
this.moveSpeed = options.moveSpeed;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async moveTo(point: ScreenPoint, options: NativeMouseMoveOptions = {}): Promise<void> {
|
|
65
|
+
const current = await this.driver.getPosition();
|
|
66
|
+
const target = roundPoint(this.applyJitter(point, options));
|
|
67
|
+
|
|
68
|
+
if (options.disableHumanPath === true) {
|
|
69
|
+
await this.driver.move(target);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const moveSpeed = options.moveSpeed ?? this.moveSpeed;
|
|
74
|
+
const route = this.pathGenerator(current, target, moveSpeed === undefined ? undefined : { moveSpeed });
|
|
75
|
+
const points = route.length === 0 ? [target] : route;
|
|
76
|
+
|
|
77
|
+
for (const [index, routePoint] of points.entries()) {
|
|
78
|
+
const isLast = index === points.length - 1;
|
|
79
|
+
const nextPoint = isLast ? target : roundPoint(this.applyJitter(routePoint, options));
|
|
80
|
+
await this.driver.move(nextPoint);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async click(point: ScreenPoint, options: NativeMouseClickOptions = {}): Promise<void> {
|
|
85
|
+
await this.moveTo(point, options);
|
|
86
|
+
await this.sleep(options.delayBeforeClickMs ?? 0);
|
|
87
|
+
await this.driver.click(options.button ?? 'left');
|
|
88
|
+
await this.sleep(options.delayAfterMoveMs ?? 0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async clickRegion(region: NativeRegion, options: NativeMouseClickOptions = {}): Promise<void> {
|
|
92
|
+
const padding = options.regionPaddingPx ?? 4;
|
|
93
|
+
const point = randomPointInRegion(region, this.random, padding);
|
|
94
|
+
const target = options.precise === true ? point : clampPointToRegion(this.applyJitter(point, options), region);
|
|
95
|
+
await this.click(roundPoint(target), { ...options, precise: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async clickBrowserPoint(page: PageLike, point: BrowserPoint, options: NativeMouseClickOptions = {}): Promise<void> {
|
|
99
|
+
const screenPoint = await this.options.coordinateMapper.browserToScreen(point, { page });
|
|
100
|
+
await this.click(screenPoint, options);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private applyJitter(point: NativePoint, options: NativeMouseMoveOptions): NativePoint {
|
|
104
|
+
if (options.precise === true) return point;
|
|
105
|
+
|
|
106
|
+
const { min, max } = normalizeJitter(options.minJitterPixels ?? this.minJitterPixels, options.jitterPixels ?? this.jitterPixels);
|
|
107
|
+
if (max <= 0) return point;
|
|
108
|
+
|
|
109
|
+
const distance = min + this.random() * (max - min);
|
|
110
|
+
const angle = this.random() * Math.PI * 2;
|
|
111
|
+
return {
|
|
112
|
+
x: point.x + Math.cos(angle) * distance,
|
|
113
|
+
y: point.y + Math.sin(angle) * distance
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { Logger } from '../logging/Logger.js';
|
|
2
|
+
import type { NativeScreenDriver, NativeWindowDriver, NativeWindowHandle } from './drivers.js';
|
|
3
|
+
import { NativeAutomationError, NativeWindowNotFoundError } from './errors.js';
|
|
4
|
+
import { matchesWindow, matcherToString } from './WindowMatcher.js';
|
|
5
|
+
import type {
|
|
6
|
+
EnsureWindowInViewOptions,
|
|
7
|
+
NativeImageFinder,
|
|
8
|
+
NativeMouse,
|
|
9
|
+
NativeRegion,
|
|
10
|
+
NativeWindowInfo,
|
|
11
|
+
WaitOptions,
|
|
12
|
+
WindowButtonOptions,
|
|
13
|
+
WindowMatcher
|
|
14
|
+
} from './types.js';
|
|
15
|
+
import { centerOf, visibleEnough } from './utils/geometry.js';
|
|
16
|
+
import { waitFor } from './utils/waitFor.js';
|
|
17
|
+
|
|
18
|
+
export interface NativeWindowServiceDependencies {
|
|
19
|
+
driver: NativeWindowDriver;
|
|
20
|
+
screen: NativeScreenDriver;
|
|
21
|
+
mouse?: NativeMouse;
|
|
22
|
+
images?: NativeImageFinder;
|
|
23
|
+
logger?: Logger;
|
|
24
|
+
defaultTimeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class DefaultNativeWindowService {
|
|
28
|
+
private readonly defaultTimeoutMs: number;
|
|
29
|
+
|
|
30
|
+
constructor(private readonly deps: NativeWindowServiceDependencies) {
|
|
31
|
+
this.defaultTimeoutMs = deps.defaultTimeoutMs ?? 15_000;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async list(): Promise<NativeWindowInfo[]> {
|
|
35
|
+
const handles = await this.deps.driver.list();
|
|
36
|
+
return Promise.all(handles.map(handle => handle.info()));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async find(matcher: WindowMatcher, options: WaitOptions = {}): Promise<NativeWindowInfo | null> {
|
|
40
|
+
if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
|
|
41
|
+
try {
|
|
42
|
+
return await this.waitFor(matcher, options);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof NativeWindowNotFoundError) return null;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const handle = await this.findHandle(matcher);
|
|
50
|
+
return handle === null ? null : handle.info();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async waitFor(matcher: WindowMatcher, options: WaitOptions = {}): Promise<NativeWindowInfo> {
|
|
54
|
+
try {
|
|
55
|
+
const handle = await this.waitForHandle(matcher, options);
|
|
56
|
+
return handle.info();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error instanceof NativeWindowNotFoundError) throw error;
|
|
59
|
+
throw new NativeWindowNotFoundError({ matcher: matcherToString(matcher), cause: error });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async isOpen(matcher: WindowMatcher): Promise<boolean> {
|
|
64
|
+
return (await this.findHandle(matcher)) !== null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async focus(matcher: WindowMatcher): Promise<NativeWindowInfo> {
|
|
68
|
+
const handle = await this.waitForHandle(matcher);
|
|
69
|
+
const before = await handle.info();
|
|
70
|
+
if (before.isMinimized === true) {
|
|
71
|
+
await handle.restore();
|
|
72
|
+
}
|
|
73
|
+
await handle.focus();
|
|
74
|
+
return this.refreshHandleInfo(handle);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async ensureInView(matcher: WindowMatcher, options: EnsureWindowInViewOptions = {}): Promise<NativeWindowInfo> {
|
|
78
|
+
const restore = options.restore ?? true;
|
|
79
|
+
const focus = options.focus ?? true;
|
|
80
|
+
const moveIntoPrimaryDisplay = options.moveIntoPrimaryDisplay ?? true;
|
|
81
|
+
const minimumVisibleWidth = options.minimumVisibleWidth ?? 100;
|
|
82
|
+
const minimumVisibleHeight = options.minimumVisibleHeight ?? 80;
|
|
83
|
+
|
|
84
|
+
const handle = await this.waitForHandle(matcher, options);
|
|
85
|
+
let info = await handle.info();
|
|
86
|
+
|
|
87
|
+
if (restore && info.isMinimized === true) {
|
|
88
|
+
await handle.restore();
|
|
89
|
+
info = await handle.info();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (focus) {
|
|
93
|
+
await handle.focus();
|
|
94
|
+
info = await handle.info();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (moveIntoPrimaryDisplay) {
|
|
98
|
+
const primaryDisplay: NativeRegion = {
|
|
99
|
+
x: 0,
|
|
100
|
+
y: 0,
|
|
101
|
+
width: await this.deps.screen.width(),
|
|
102
|
+
height: await this.deps.screen.height()
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (!visibleEnough(info.region, primaryDisplay, minimumVisibleWidth, minimumVisibleHeight)) {
|
|
106
|
+
const target = {
|
|
107
|
+
x: Math.max(0, Math.min(info.region.x, Math.max(0, primaryDisplay.width - info.region.width))),
|
|
108
|
+
y: Math.max(0, Math.min(info.region.y, Math.max(0, primaryDisplay.height - info.region.height)))
|
|
109
|
+
};
|
|
110
|
+
await handle.move(target);
|
|
111
|
+
info = await handle.info();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return info;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async setFullscreen(matcher: WindowMatcher, fullscreen: boolean, options: WindowButtonOptions = {}): Promise<void> {
|
|
119
|
+
const handle = await this.waitForHandle(matcher, options);
|
|
120
|
+
const info = await handle.info();
|
|
121
|
+
|
|
122
|
+
if (info.isFullscreen === fullscreen) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (handle.setFullscreen !== undefined) {
|
|
127
|
+
await handle.setFullscreen(fullscreen);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (handle.toggleFullscreen !== undefined) {
|
|
132
|
+
await handle.toggleFullscreen();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if ((options.strategy ?? 'auto') !== 'accessibility' && this.deps.images !== undefined && this.deps.mouse !== undefined && options.fullscreenButtonTemplate !== undefined) {
|
|
137
|
+
await this.clickWindowButtonTemplate(info.region, options.fullscreenButtonTemplate, options);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
throw new NativeAutomationError('Window fullscreen control is unavailable for the matched window.', {
|
|
142
|
+
matcher: matcherToString(matcher),
|
|
143
|
+
strategy: options.strategy ?? 'auto'
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async toggleFullscreen(matcher: WindowMatcher, options: WindowButtonOptions = {}): Promise<void> {
|
|
148
|
+
const handle = await this.waitForHandle(matcher, options);
|
|
149
|
+
const info = await handle.info();
|
|
150
|
+
|
|
151
|
+
if (handle.toggleFullscreen !== undefined) {
|
|
152
|
+
await handle.toggleFullscreen();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (handle.setFullscreen !== undefined && info.isFullscreen !== undefined) {
|
|
157
|
+
await handle.setFullscreen(!info.isFullscreen);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if ((options.strategy ?? 'auto') !== 'accessibility' && this.deps.images !== undefined && this.deps.mouse !== undefined && options.fullscreenButtonTemplate !== undefined) {
|
|
162
|
+
await this.clickWindowButtonTemplate(info.region, options.fullscreenButtonTemplate, options);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
throw new NativeAutomationError('Window fullscreen toggle is unavailable for the matched window.', {
|
|
167
|
+
matcher: matcherToString(matcher),
|
|
168
|
+
strategy: options.strategy ?? 'auto'
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async minimize(matcher: WindowMatcher): Promise<void> {
|
|
173
|
+
const handle = await this.waitForHandle(matcher);
|
|
174
|
+
await handle.minimize();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async restore(matcher: WindowMatcher): Promise<void> {
|
|
178
|
+
const handle = await this.waitForHandle(matcher);
|
|
179
|
+
await handle.restore();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private async findHandle(matcher: WindowMatcher): Promise<NativeWindowHandle | null> {
|
|
183
|
+
const handles = await this.deps.driver.list();
|
|
184
|
+
for (const handle of handles) {
|
|
185
|
+
const info = await handle.info();
|
|
186
|
+
if (matchesWindow(info, matcher)) {
|
|
187
|
+
return handle;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private async waitForHandle(matcher: WindowMatcher, options: WaitOptions = {}): Promise<NativeWindowHandle> {
|
|
194
|
+
try {
|
|
195
|
+
return await waitFor(
|
|
196
|
+
() => this.findHandle(matcher),
|
|
197
|
+
{
|
|
198
|
+
timeoutMs: options.timeoutMs ?? this.defaultTimeoutMs,
|
|
199
|
+
intervalMs: options.intervalMs ?? 100,
|
|
200
|
+
description: `native window (${matcherToString(matcher)})`
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
throw new NativeWindowNotFoundError({ matcher: matcherToString(matcher), cause: error });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async refreshHandleInfo(handle: NativeWindowHandle): Promise<NativeWindowInfo> {
|
|
209
|
+
return handle.info();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private async clickWindowButtonTemplate(region: NativeRegion, template: string, options: WindowButtonOptions): Promise<void> {
|
|
213
|
+
if (this.deps.images === undefined || this.deps.mouse === undefined) {
|
|
214
|
+
throw new NativeAutomationError('Visual window button strategy requires image finder and native mouse dependencies.');
|
|
215
|
+
}
|
|
216
|
+
const padding = options.searchRegionPaddingPx ?? 96;
|
|
217
|
+
const searchRegion: NativeRegion = {
|
|
218
|
+
x: region.x,
|
|
219
|
+
y: region.y,
|
|
220
|
+
width: Math.min(region.width, padding),
|
|
221
|
+
height: Math.min(region.height, padding)
|
|
222
|
+
};
|
|
223
|
+
const imageOptions: { searchRegion: NativeRegion; timeoutMs?: number; intervalMs?: number } = { searchRegion };
|
|
224
|
+
if (options.timeoutMs !== undefined) imageOptions.timeoutMs = options.timeoutMs;
|
|
225
|
+
if (options.intervalMs !== undefined) imageOptions.intervalMs = options.intervalMs;
|
|
226
|
+
const match = await this.deps.images.waitForTemplate(template, imageOptions);
|
|
227
|
+
await this.deps.mouse.click(centerOf(match.region));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BrowserPoint,
|
|
3
|
+
CalibrationOptions,
|
|
4
|
+
CoordinateMapOptions,
|
|
5
|
+
CoordinateTransform,
|
|
6
|
+
NativeAutomation,
|
|
7
|
+
NativeFileDialogService,
|
|
8
|
+
NativeImageFinder,
|
|
9
|
+
NativeImageMatch,
|
|
10
|
+
NativeImageSearchOptions,
|
|
11
|
+
NativeKeyboard,
|
|
12
|
+
NativeMouse,
|
|
13
|
+
NativeMouseClickOptions,
|
|
14
|
+
NativeMouseMoveOptions,
|
|
15
|
+
NativeRegion,
|
|
16
|
+
NativeTemplateRef,
|
|
17
|
+
NativeWindowInfo,
|
|
18
|
+
NativeWindowService,
|
|
19
|
+
ScreenPoint,
|
|
20
|
+
SelectFileOptions,
|
|
21
|
+
WaitOptions,
|
|
22
|
+
WindowButtonOptions,
|
|
23
|
+
WindowMatcher
|
|
24
|
+
} from './types.js';
|
|
25
|
+
import { DefaultNativeActionRegistry, registerDefaultNativeActions } from './NativeActionRegistry.js';
|
|
26
|
+
import { UnsupportedPlatformError } from './errors.js';
|
|
27
|
+
import type { Logger } from '../logging/Logger.js';
|
|
28
|
+
import type { PageLike } from '../browser/PuppeteerLike.js';
|
|
29
|
+
|
|
30
|
+
class UnsupportedNativeWindowService implements NativeWindowService {
|
|
31
|
+
constructor(private readonly platform: NodeJS.Platform) {}
|
|
32
|
+
async list(): Promise<NativeWindowInfo[]> { throw new UnsupportedPlatformError(this.platform); }
|
|
33
|
+
async find(_matcher: WindowMatcher, _options?: WaitOptions): Promise<NativeWindowInfo | null> { throw new UnsupportedPlatformError(this.platform); }
|
|
34
|
+
async waitFor(_matcher: WindowMatcher, _options?: WaitOptions): Promise<NativeWindowInfo> { throw new UnsupportedPlatformError(this.platform); }
|
|
35
|
+
async isOpen(_matcher: WindowMatcher): Promise<boolean> { throw new UnsupportedPlatformError(this.platform); }
|
|
36
|
+
async focus(_matcher: WindowMatcher): Promise<NativeWindowInfo> { throw new UnsupportedPlatformError(this.platform); }
|
|
37
|
+
async ensureInView(_matcher: WindowMatcher): Promise<NativeWindowInfo> { throw new UnsupportedPlatformError(this.platform); }
|
|
38
|
+
async setFullscreen(_matcher: WindowMatcher, _fullscreen: boolean, _options?: WindowButtonOptions): Promise<void> { throw new UnsupportedPlatformError(this.platform); }
|
|
39
|
+
async toggleFullscreen(_matcher: WindowMatcher, _options?: WindowButtonOptions): Promise<void> { throw new UnsupportedPlatformError(this.platform); }
|
|
40
|
+
async minimize(_matcher: WindowMatcher): Promise<void> { throw new UnsupportedPlatformError(this.platform); }
|
|
41
|
+
async restore(_matcher: WindowMatcher): Promise<void> { throw new UnsupportedPlatformError(this.platform); }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class UnsupportedNativeFileDialogService implements NativeFileDialogService {
|
|
45
|
+
constructor(private readonly platform: NodeJS.Platform) {}
|
|
46
|
+
async selectFile(_filePath: string, _options?: SelectFileOptions): Promise<void> { throw new UnsupportedPlatformError(this.platform); }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class UnsupportedNativeMouse implements NativeMouse {
|
|
50
|
+
constructor(private readonly platform: NodeJS.Platform) {}
|
|
51
|
+
async moveTo(_point: ScreenPoint, _options?: NativeMouseMoveOptions): Promise<void> { throw new UnsupportedPlatformError(this.platform); }
|
|
52
|
+
async click(_point: ScreenPoint, _options?: NativeMouseClickOptions): Promise<void> { throw new UnsupportedPlatformError(this.platform); }
|
|
53
|
+
async clickRegion(_region: NativeRegion, _options?: NativeMouseClickOptions): Promise<void> { throw new UnsupportedPlatformError(this.platform); }
|
|
54
|
+
async clickBrowserPoint(_page: PageLike, _point: BrowserPoint, _options?: NativeMouseClickOptions): Promise<void> { throw new UnsupportedPlatformError(this.platform); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class UnsupportedNativeKeyboard implements NativeKeyboard {
|
|
58
|
+
constructor(private readonly platform: NodeJS.Platform) {}
|
|
59
|
+
async type(_text: string): Promise<void> { throw new UnsupportedPlatformError(this.platform); }
|
|
60
|
+
async pressKey(_key: string): Promise<void> { throw new UnsupportedPlatformError(this.platform); }
|
|
61
|
+
async pressShortcut(_keys: readonly string[]): Promise<void> { throw new UnsupportedPlatformError(this.platform); }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class UnsupportedNativeCoordinateMapper {
|
|
65
|
+
constructor(private readonly platform: NodeJS.Platform) {}
|
|
66
|
+
async calibrate(_page: PageLike, _options?: CalibrationOptions): Promise<CoordinateTransform> { throw new UnsupportedPlatformError(this.platform); }
|
|
67
|
+
async browserToScreen(_point: BrowserPoint, _options?: CoordinateMapOptions): Promise<ScreenPoint> { throw new UnsupportedPlatformError(this.platform); }
|
|
68
|
+
async screenToBrowser(_point: ScreenPoint, _options?: CoordinateMapOptions): Promise<BrowserPoint> { throw new UnsupportedPlatformError(this.platform); }
|
|
69
|
+
invalidate(): void {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class UnsupportedNativeImageFinder implements NativeImageFinder {
|
|
73
|
+
constructor(private readonly platform: NodeJS.Platform) {}
|
|
74
|
+
async findTemplate(_template: string | NativeTemplateRef, _options?: NativeImageSearchOptions): Promise<NativeImageMatch | null> { throw new UnsupportedPlatformError(this.platform); }
|
|
75
|
+
async waitForTemplate(_template: string | NativeTemplateRef, _options?: NativeImageSearchOptions): Promise<NativeImageMatch> { throw new UnsupportedPlatformError(this.platform); }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createUnsupportedNativeAutomation(platform: NodeJS.Platform, logger: Logger): NativeAutomation {
|
|
79
|
+
const automation = {
|
|
80
|
+
windows: new UnsupportedNativeWindowService(platform),
|
|
81
|
+
fileDialogs: new UnsupportedNativeFileDialogService(platform),
|
|
82
|
+
mouse: new UnsupportedNativeMouse(platform),
|
|
83
|
+
keyboard: new UnsupportedNativeKeyboard(platform),
|
|
84
|
+
coordinates: new UnsupportedNativeCoordinateMapper(platform),
|
|
85
|
+
images: new UnsupportedNativeImageFinder(platform),
|
|
86
|
+
actions: undefined as unknown as DefaultNativeActionRegistry
|
|
87
|
+
} satisfies Omit<NativeAutomation, 'actions'> & { actions: DefaultNativeActionRegistry };
|
|
88
|
+
|
|
89
|
+
const registry = new DefaultNativeActionRegistry(() => ({ automation, logger }));
|
|
90
|
+
automation.actions = registerDefaultNativeActions(registry, automation, logger);
|
|
91
|
+
return automation;
|
|
92
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { NativeWindowInfo, WindowMatcher } from './types.js';
|
|
2
|
+
|
|
3
|
+
function matchesRegex(value: string, regex: RegExp | string): boolean {
|
|
4
|
+
const expression = typeof regex === 'string' ? new RegExp(regex) : regex;
|
|
5
|
+
return expression.test(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function matchesOptional(actual: string | undefined, expected: string | undefined): boolean {
|
|
9
|
+
return expected === undefined || actual === expected;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function matchesWindow(info: NativeWindowInfo, matcher: WindowMatcher): boolean {
|
|
13
|
+
if (!matchesOptional(info.appName, matcher.appName)) return false;
|
|
14
|
+
if (!matchesOptional(info.bundleId, matcher.bundleId)) return false;
|
|
15
|
+
if (!matchesOptional(info.role, matcher.role)) return false;
|
|
16
|
+
if (matcher.title !== undefined && info.title !== matcher.title) return false;
|
|
17
|
+
if (matcher.titleIncludes !== undefined && !info.title.includes(matcher.titleIncludes)) return false;
|
|
18
|
+
if (matcher.titleRegex !== undefined && !matchesRegex(info.title, matcher.titleRegex)) return false;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function matcherToString(matcher: WindowMatcher): string {
|
|
23
|
+
const parts: string[] = [];
|
|
24
|
+
if (matcher.appName !== undefined) parts.push(`app=${matcher.appName}`);
|
|
25
|
+
if (matcher.bundleId !== undefined) parts.push(`bundle=${matcher.bundleId}`);
|
|
26
|
+
if (matcher.title !== undefined) parts.push(`title=${matcher.title}`);
|
|
27
|
+
if (matcher.titleIncludes !== undefined) parts.push(`titleIncludes=${matcher.titleIncludes}`);
|
|
28
|
+
if (matcher.titleRegex !== undefined) parts.push(`titleRegex=${String(matcher.titleRegex)}`);
|
|
29
|
+
if (matcher.role !== undefined) parts.push(`role=${matcher.role}`);
|
|
30
|
+
return parts.length === 0 ? '<any window>' : parts.join(', ');
|
|
31
|
+
}
|