hyper-agent-browser 0.1.0
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/LICENSE +21 -0
- package/README.md +196 -0
- package/package.json +63 -0
- package/src/browser/context.ts +66 -0
- package/src/browser/manager.ts +414 -0
- package/src/browser/sync-chrome-data.ts +53 -0
- package/src/cli.ts +628 -0
- package/src/cli.ts.backup +529 -0
- package/src/commands/actions.ts +232 -0
- package/src/commands/advanced.ts +252 -0
- package/src/commands/config.ts +44 -0
- package/src/commands/getters.ts +110 -0
- package/src/commands/info.ts +195 -0
- package/src/commands/navigation.ts +50 -0
- package/src/commands/session.ts +83 -0
- package/src/daemon/browser-pool.ts +65 -0
- package/src/daemon/client.ts +128 -0
- package/src/daemon/main.ts +200 -0
- package/src/daemon/server.ts +562 -0
- package/src/session/manager.ts +110 -0
- package/src/session/store.ts +172 -0
- package/src/snapshot/accessibility.ts +182 -0
- package/src/snapshot/dom-extractor.ts +220 -0
- package/src/snapshot/formatter.ts +115 -0
- package/src/snapshot/reference-store.ts +97 -0
- package/src/utils/config.ts +183 -0
- package/src/utils/errors.ts +121 -0
- package/src/utils/logger.ts +12 -0
- package/src/utils/selector.ts +23 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { Locator, Page } from "patchright";
|
|
2
|
+
import { parseSelector } from "../utils/selector";
|
|
3
|
+
|
|
4
|
+
// Global reference store for element mappings
|
|
5
|
+
let globalReferenceStore: any = null;
|
|
6
|
+
|
|
7
|
+
export function setReferenceStore(store: any) {
|
|
8
|
+
globalReferenceStore = store;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function getLocator(page: Page, selector: string): Promise<Locator> {
|
|
12
|
+
const parsed = parseSelector(selector);
|
|
13
|
+
|
|
14
|
+
switch (parsed.type) {
|
|
15
|
+
case "ref": {
|
|
16
|
+
// Try to resolve @eN reference to actual selector
|
|
17
|
+
if (!globalReferenceStore) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`Element reference @${parsed.value} requires a snapshot first. Run 'hab snapshot -i' to generate element references.`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const actualSelector = globalReferenceStore.get(parsed.value);
|
|
24
|
+
if (!actualSelector) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Element reference @${parsed.value} not found. Run 'hab snapshot -i' to update element references.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Use the mapped CSS selector
|
|
31
|
+
return page.locator(actualSelector);
|
|
32
|
+
}
|
|
33
|
+
case "css":
|
|
34
|
+
return page.locator(parsed.value);
|
|
35
|
+
case "text":
|
|
36
|
+
return page.getByText(parsed.value);
|
|
37
|
+
case "xpath":
|
|
38
|
+
return page.locator(`xpath=${parsed.value}`);
|
|
39
|
+
default:
|
|
40
|
+
throw new Error(`Unknown selector type: ${parsed.type}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function click(page: Page, selector: string): Promise<void> {
|
|
45
|
+
const locator = await getLocator(page, selector);
|
|
46
|
+
await locator.click();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function fill(page: Page, selector: string, value: string): Promise<void> {
|
|
50
|
+
const locator = await getLocator(page, selector);
|
|
51
|
+
await locator.fill(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function type(page: Page, selector: string, text: string, delay = 0): Promise<void> {
|
|
55
|
+
const locator = await getLocator(page, selector);
|
|
56
|
+
await locator.pressSequentially(text, { delay });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function press(page: Page, key: string): Promise<void> {
|
|
60
|
+
await page.keyboard.press(key);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function scroll(
|
|
64
|
+
page: Page,
|
|
65
|
+
direction: "up" | "down" | "left" | "right",
|
|
66
|
+
amount = 500,
|
|
67
|
+
selector?: string,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
if (selector) {
|
|
70
|
+
const locator = await getLocator(page, selector);
|
|
71
|
+
const element = await locator.elementHandle();
|
|
72
|
+
if (!element) {
|
|
73
|
+
throw new Error(`Element not found: ${selector}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await element.evaluate(
|
|
77
|
+
(el, { direction, amount }) => {
|
|
78
|
+
const scrollMap = {
|
|
79
|
+
down: { top: amount, left: 0 },
|
|
80
|
+
up: { top: -amount, left: 0 },
|
|
81
|
+
right: { top: 0, left: amount },
|
|
82
|
+
left: { top: 0, left: -amount },
|
|
83
|
+
};
|
|
84
|
+
const scroll = scrollMap[direction as keyof typeof scrollMap];
|
|
85
|
+
el.scrollBy(scroll);
|
|
86
|
+
},
|
|
87
|
+
{ direction, amount },
|
|
88
|
+
);
|
|
89
|
+
} else {
|
|
90
|
+
await page.evaluate(
|
|
91
|
+
({ direction, amount }) => {
|
|
92
|
+
const scrollMap = {
|
|
93
|
+
down: { top: amount, left: 0 },
|
|
94
|
+
up: { top: -amount, left: 0 },
|
|
95
|
+
right: { top: 0, left: amount },
|
|
96
|
+
left: { top: 0, left: -amount },
|
|
97
|
+
};
|
|
98
|
+
const scroll = scrollMap[direction as keyof typeof scrollMap];
|
|
99
|
+
// @ts-ignore - window is available in browser context
|
|
100
|
+
window.scrollBy(scroll);
|
|
101
|
+
},
|
|
102
|
+
{ direction, amount },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function hover(page: Page, selector: string): Promise<void> {
|
|
108
|
+
const locator = await getLocator(page, selector);
|
|
109
|
+
await locator.hover();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function select(page: Page, selector: string, value: string): Promise<void> {
|
|
113
|
+
const locator = await getLocator(page, selector);
|
|
114
|
+
await locator.selectOption(value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface WaitOptions {
|
|
118
|
+
timeout?: number;
|
|
119
|
+
text?: string;
|
|
120
|
+
url?: string;
|
|
121
|
+
fn?: string;
|
|
122
|
+
load?: "load" | "domcontentloaded" | "networkidle";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function wait(
|
|
126
|
+
page: Page,
|
|
127
|
+
condition: string | number,
|
|
128
|
+
options: WaitOptions = {},
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
const timeout = options.timeout || 30000;
|
|
131
|
+
|
|
132
|
+
// Wait for milliseconds
|
|
133
|
+
if (typeof condition === "number") {
|
|
134
|
+
await page.waitForTimeout(condition);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Wait for navigation
|
|
139
|
+
if (condition === "navigation") {
|
|
140
|
+
await page.waitForLoadState("load", { timeout });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Wait for load state (--load networkidle)
|
|
145
|
+
if (options.load) {
|
|
146
|
+
await page.waitForLoadState(options.load, { timeout });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Wait for text to appear (--text "Welcome")
|
|
151
|
+
if (options.text) {
|
|
152
|
+
await page.waitForSelector(`text=${options.text}`, { timeout });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Wait for URL pattern (--url "**/dashboard")
|
|
157
|
+
if (options.url) {
|
|
158
|
+
await page.waitForURL(options.url, { timeout });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Wait for JavaScript condition (--fn "window.loaded === true")
|
|
163
|
+
if (options.fn) {
|
|
164
|
+
await page.waitForFunction(options.fn, { timeout });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Wait for selector to be visible
|
|
169
|
+
if (condition.startsWith("selector=")) {
|
|
170
|
+
const selector = condition.slice(9);
|
|
171
|
+
const locator = await getLocator(page, selector);
|
|
172
|
+
await locator.waitFor({ state: "visible", timeout });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Wait for selector to be hidden
|
|
177
|
+
if (condition.startsWith("hidden=")) {
|
|
178
|
+
const selector = condition.slice(7);
|
|
179
|
+
const locator = await getLocator(page, selector);
|
|
180
|
+
await locator.waitFor({ state: "hidden", timeout });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throw new Error(`Unknown wait condition: ${condition}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ============ 第一批高优先级功能 ============
|
|
188
|
+
|
|
189
|
+
export async function check(page: Page, selector: string): Promise<void> {
|
|
190
|
+
const locator = await getLocator(page, selector);
|
|
191
|
+
await locator.check();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function uncheck(page: Page, selector: string): Promise<void> {
|
|
195
|
+
const locator = await getLocator(page, selector);
|
|
196
|
+
await locator.uncheck();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function dblclick(page: Page, selector: string): Promise<void> {
|
|
200
|
+
const locator = await getLocator(page, selector);
|
|
201
|
+
await locator.dblclick();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function focus(page: Page, selector: string): Promise<void> {
|
|
205
|
+
const locator = await getLocator(page, selector);
|
|
206
|
+
await locator.focus();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function upload(
|
|
210
|
+
page: Page,
|
|
211
|
+
selector: string,
|
|
212
|
+
files: string | string[],
|
|
213
|
+
): Promise<void> {
|
|
214
|
+
const locator = await getLocator(page, selector);
|
|
215
|
+
const filePaths = Array.isArray(files) ? files : [files];
|
|
216
|
+
await locator.setInputFiles(filePaths);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function scrollIntoView(page: Page, selector: string): Promise<void> {
|
|
220
|
+
const locator = await getLocator(page, selector);
|
|
221
|
+
await locator.scrollIntoViewIfNeeded();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function drag(
|
|
225
|
+
page: Page,
|
|
226
|
+
sourceSelector: string,
|
|
227
|
+
targetSelector: string,
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
const source = await getLocator(page, sourceSelector);
|
|
230
|
+
const target = await getLocator(page, targetSelector);
|
|
231
|
+
await source.dragTo(target);
|
|
232
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { Cookie, Page } from "patchright";
|
|
2
|
+
|
|
3
|
+
// ============ Dialog 对话框处理 ============
|
|
4
|
+
|
|
5
|
+
export async function setupDialogHandler(
|
|
6
|
+
page: Page,
|
|
7
|
+
action: "accept" | "dismiss",
|
|
8
|
+
promptText?: string,
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
page.on("dialog", async (dialog) => {
|
|
11
|
+
if (action === "accept") {
|
|
12
|
+
await dialog.accept(promptText);
|
|
13
|
+
} else {
|
|
14
|
+
await dialog.dismiss();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function dialogAccept(page: Page, text?: string): Promise<void> {
|
|
20
|
+
await setupDialogHandler(page, "accept", text);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function dialogDismiss(page: Page): Promise<void> {
|
|
24
|
+
await setupDialogHandler(page, "dismiss");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ============ Cookie 管理 ============
|
|
28
|
+
|
|
29
|
+
export async function getCookies(page: Page): Promise<Cookie[]> {
|
|
30
|
+
const context = page.context();
|
|
31
|
+
return await context.cookies();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function setCookie(page: Page, name: string, value: string): Promise<void> {
|
|
35
|
+
const context = page.context();
|
|
36
|
+
const url = page.url();
|
|
37
|
+
|
|
38
|
+
await context.addCookies([
|
|
39
|
+
{
|
|
40
|
+
name,
|
|
41
|
+
value,
|
|
42
|
+
url,
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function clearCookies(page: Page): Promise<void> {
|
|
48
|
+
const context = page.context();
|
|
49
|
+
await context.clearCookies();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============ Storage 管理 ============
|
|
53
|
+
|
|
54
|
+
export async function getLocalStorage(page: Page, key?: string): Promise<any> {
|
|
55
|
+
if (key) {
|
|
56
|
+
return await page.evaluate((k) => localStorage.getItem(k), key);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return await page.evaluate(() => {
|
|
60
|
+
const items: Record<string, string> = {};
|
|
61
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
62
|
+
const key = localStorage.key(i);
|
|
63
|
+
if (key) {
|
|
64
|
+
items[key] = localStorage.getItem(key) || "";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return items;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function setLocalStorage(page: Page, key: string, value: string): Promise<void> {
|
|
72
|
+
await page.evaluate(({ k, v }) => localStorage.setItem(k, v), { k: key, v: value });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function clearLocalStorage(page: Page): Promise<void> {
|
|
76
|
+
await page.evaluate(() => localStorage.clear());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function getSessionStorage(page: Page, key?: string): Promise<any> {
|
|
80
|
+
if (key) {
|
|
81
|
+
return await page.evaluate((k) => sessionStorage.getItem(k), key);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return await page.evaluate(() => {
|
|
85
|
+
const items: Record<string, string> = {};
|
|
86
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
87
|
+
const key = sessionStorage.key(i);
|
|
88
|
+
if (key) {
|
|
89
|
+
items[key] = sessionStorage.getItem(key) || "";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return items;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function setSessionStorage(page: Page, key: string, value: string): Promise<void> {
|
|
97
|
+
await page.evaluate(({ k, v }) => sessionStorage.setItem(k, v), { k: key, v: value });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function clearSessionStorage(page: Page): Promise<void> {
|
|
101
|
+
await page.evaluate(() => sessionStorage.clear());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============ PDF 导出 ============
|
|
105
|
+
|
|
106
|
+
export interface PDFOptions {
|
|
107
|
+
path: string;
|
|
108
|
+
format?:
|
|
109
|
+
| "Letter"
|
|
110
|
+
| "Legal"
|
|
111
|
+
| "Tabloid"
|
|
112
|
+
| "Ledger"
|
|
113
|
+
| "A0"
|
|
114
|
+
| "A1"
|
|
115
|
+
| "A2"
|
|
116
|
+
| "A3"
|
|
117
|
+
| "A4"
|
|
118
|
+
| "A5"
|
|
119
|
+
| "A6";
|
|
120
|
+
landscape?: boolean;
|
|
121
|
+
printBackground?: boolean;
|
|
122
|
+
margin?: {
|
|
123
|
+
top?: string;
|
|
124
|
+
right?: string;
|
|
125
|
+
bottom?: string;
|
|
126
|
+
left?: string;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function savePDF(page: Page, options: PDFOptions): Promise<string> {
|
|
131
|
+
await page.pdf({
|
|
132
|
+
path: options.path,
|
|
133
|
+
format: options.format || "A4",
|
|
134
|
+
landscape: options.landscape || false,
|
|
135
|
+
printBackground: options.printBackground !== false,
|
|
136
|
+
margin: options.margin,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return options.path;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ============ 设备模拟 ============
|
|
143
|
+
|
|
144
|
+
export async function setViewport(page: Page, width: number, height: number): Promise<void> {
|
|
145
|
+
await page.setViewportSize({ width, height });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function setGeolocation(
|
|
149
|
+
page: Page,
|
|
150
|
+
latitude: number,
|
|
151
|
+
longitude: number,
|
|
152
|
+
): Promise<void> {
|
|
153
|
+
const context = page.context();
|
|
154
|
+
await context.setGeolocation({ latitude, longitude });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function setOffline(page: Page, offline: boolean): Promise<void> {
|
|
158
|
+
const context = page.context();
|
|
159
|
+
await context.setOffline(offline);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function setExtraHeaders(page: Page, headers: Record<string, string>): Promise<void> {
|
|
163
|
+
const context = page.context();
|
|
164
|
+
await context.setExtraHTTPHeaders(headers);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function setMediaColorScheme(
|
|
168
|
+
page: Page,
|
|
169
|
+
scheme: "light" | "dark" | "no-preference",
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
await page.emulateMedia({ colorScheme: scheme });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ============ 鼠标控制 ============
|
|
175
|
+
|
|
176
|
+
export async function mouseMove(page: Page, x: number, y: number): Promise<void> {
|
|
177
|
+
await page.mouse.move(x, y);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function mouseDown(
|
|
181
|
+
page: Page,
|
|
182
|
+
button: "left" | "right" | "middle" = "left",
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
await page.mouse.down({ button });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function mouseUp(
|
|
188
|
+
page: Page,
|
|
189
|
+
button: "left" | "right" | "middle" = "left",
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
await page.mouse.up({ button });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function mouseWheel(page: Page, deltaY: number, deltaX = 0): Promise<void> {
|
|
195
|
+
await page.mouse.wheel(deltaX, deltaY);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ============ 键盘控制 ============
|
|
199
|
+
|
|
200
|
+
export async function keyDown(page: Page, key: string): Promise<void> {
|
|
201
|
+
await page.keyboard.down(key);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function keyUp(page: Page, key: string): Promise<void> {
|
|
205
|
+
await page.keyboard.up(key);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ============ Console & Errors ============
|
|
209
|
+
|
|
210
|
+
const consoleLogs: string[] = [];
|
|
211
|
+
const pageErrors: string[] = [];
|
|
212
|
+
|
|
213
|
+
export function setupConsoleCapture(page: Page): void {
|
|
214
|
+
page.on("console", (msg) => {
|
|
215
|
+
consoleLogs.push(`[${msg.type()}] ${msg.text()}`);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function setupErrorCapture(page: Page): void {
|
|
220
|
+
page.on("pageerror", (error) => {
|
|
221
|
+
pageErrors.push(error.message);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function getConsoleLogs(): string[] {
|
|
226
|
+
return [...consoleLogs];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function clearConsoleLogs(): void {
|
|
230
|
+
consoleLogs.length = 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function getPageErrors(): string[] {
|
|
234
|
+
return [...pageErrors];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function clearPageErrors(): void {
|
|
238
|
+
pageErrors.length = 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ============ 元素高亮 ============
|
|
242
|
+
|
|
243
|
+
export async function highlightElement(page: Page, selector: string): Promise<void> {
|
|
244
|
+
await page.evaluate((sel) => {
|
|
245
|
+
const element = document.querySelector(sel);
|
|
246
|
+
if (element && element instanceof HTMLElement) {
|
|
247
|
+
element.style.outline = "3px solid red";
|
|
248
|
+
element.style.outlineOffset = "2px";
|
|
249
|
+
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
250
|
+
}
|
|
251
|
+
}, selector);
|
|
252
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { getConfigValue, loadConfig, setConfigValue } from "../utils/config";
|
|
2
|
+
|
|
3
|
+
export async function listConfig(): Promise<string> {
|
|
4
|
+
const config = await loadConfig();
|
|
5
|
+
return JSON.stringify(config, null, 2);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function getConfig(key: string): Promise<string> {
|
|
9
|
+
const value = await getConfigValue(key);
|
|
10
|
+
|
|
11
|
+
if (value === undefined) {
|
|
12
|
+
throw new Error(`Config key not found: ${key}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (typeof value === "object") {
|
|
16
|
+
return JSON.stringify(value, null, 2);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return String(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function setConfig(key: string, value: string): Promise<string> {
|
|
23
|
+
// Parse value
|
|
24
|
+
let parsedValue: unknown = value;
|
|
25
|
+
|
|
26
|
+
// Try to parse as JSON first
|
|
27
|
+
if (value === "true") {
|
|
28
|
+
parsedValue = true;
|
|
29
|
+
} else if (value === "false") {
|
|
30
|
+
parsedValue = false;
|
|
31
|
+
} else if (!Number.isNaN(Number(value))) {
|
|
32
|
+
parsedValue = Number(value);
|
|
33
|
+
} else {
|
|
34
|
+
try {
|
|
35
|
+
parsedValue = JSON.parse(value);
|
|
36
|
+
} catch {
|
|
37
|
+
// Keep as string
|
|
38
|
+
parsedValue = value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await setConfigValue(key, parsedValue);
|
|
43
|
+
return `Config updated: ${key} = ${JSON.stringify(parsedValue)}`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Page } from "patchright";
|
|
2
|
+
import { parseSelector } from "../utils/selector";
|
|
3
|
+
|
|
4
|
+
// Global reference store for element mappings
|
|
5
|
+
let globalReferenceStore: any = null;
|
|
6
|
+
|
|
7
|
+
export function setReferenceStore(store: any) {
|
|
8
|
+
globalReferenceStore = store;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function getLocator(page: Page, selector: string) {
|
|
12
|
+
const parsed = parseSelector(selector);
|
|
13
|
+
|
|
14
|
+
switch (parsed.type) {
|
|
15
|
+
case "ref": {
|
|
16
|
+
if (!globalReferenceStore) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Element reference @${parsed.value} requires a snapshot first. Run 'hab snapshot -i' to generate element references.`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const actualSelector = globalReferenceStore.get(parsed.value);
|
|
23
|
+
if (!actualSelector) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Element reference @${parsed.value} not found. Run 'hab snapshot -i' to update element references.`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return page.locator(actualSelector);
|
|
30
|
+
}
|
|
31
|
+
case "css":
|
|
32
|
+
return page.locator(parsed.value);
|
|
33
|
+
case "text":
|
|
34
|
+
return page.getByText(parsed.value);
|
|
35
|
+
case "xpath":
|
|
36
|
+
return page.locator(`xpath=${parsed.value}`);
|
|
37
|
+
default:
|
|
38
|
+
throw new Error(`Unknown selector type: ${parsed.type}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ============ Get 系列命令 ============
|
|
43
|
+
|
|
44
|
+
export async function getText(page: Page, selector: string): Promise<string> {
|
|
45
|
+
const locator = await getLocator(page, selector);
|
|
46
|
+
const text = await locator.textContent();
|
|
47
|
+
return text || "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function getValue(page: Page, selector: string): Promise<string> {
|
|
51
|
+
const locator = await getLocator(page, selector);
|
|
52
|
+
const value = await locator.inputValue();
|
|
53
|
+
return value || "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function getAttr(page: Page, selector: string, attribute: string): Promise<string> {
|
|
57
|
+
const locator = await getLocator(page, selector);
|
|
58
|
+
const value = await locator.getAttribute(attribute);
|
|
59
|
+
return value || "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function getHtml(page: Page, selector: string): Promise<string> {
|
|
63
|
+
const locator = await getLocator(page, selector);
|
|
64
|
+
const html = await locator.innerHTML();
|
|
65
|
+
return html || "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function getCount(page: Page, selector: string): Promise<number> {
|
|
69
|
+
const locator = await getLocator(page, selector);
|
|
70
|
+
return await locator.count();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface BoundingBox {
|
|
74
|
+
x: number;
|
|
75
|
+
y: number;
|
|
76
|
+
width: number;
|
|
77
|
+
height: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function getBox(page: Page, selector: string): Promise<BoundingBox | null> {
|
|
81
|
+
const locator = await getLocator(page, selector);
|
|
82
|
+
return await locator.boundingBox();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============ Is 系列命令 (状态检查) ============
|
|
86
|
+
|
|
87
|
+
export async function isVisible(page: Page, selector: string): Promise<boolean> {
|
|
88
|
+
const locator = await getLocator(page, selector);
|
|
89
|
+
return await locator.isVisible();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function isEnabled(page: Page, selector: string): Promise<boolean> {
|
|
93
|
+
const locator = await getLocator(page, selector);
|
|
94
|
+
return await locator.isEnabled();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function isChecked(page: Page, selector: string): Promise<boolean> {
|
|
98
|
+
const locator = await getLocator(page, selector);
|
|
99
|
+
return await locator.isChecked();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function isEditable(page: Page, selector: string): Promise<boolean> {
|
|
103
|
+
const locator = await getLocator(page, selector);
|
|
104
|
+
return await locator.isEditable();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function isHidden(page: Page, selector: string): Promise<boolean> {
|
|
108
|
+
const locator = await getLocator(page, selector);
|
|
109
|
+
return await locator.isHidden();
|
|
110
|
+
}
|