@probolabs/playwright 0.1.1 → 0.1.3
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/actions.d.ts.map +1 -0
- package/dist/actions.js +241 -0
- package/dist/api-client.d.ts +34 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +125 -0
- package/dist/highlight.d.ts.map +1 -0
- package/dist/highlight.js +79 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1177 -0
- package/dist/index.js.map +1 -0
- package/dist/probo-playwright.esm.js +8 -8
- package/dist/probo-playwright.umd.js +609 -34
- package/dist/src/actions.d.ts +18 -0
- package/dist/src/api-client.d.ts +13 -0
- package/dist/src/highlight.d.ts +28 -0
- package/dist/src/index.d.ts +11 -14
- package/dist/src/utils.d.ts +20 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/actions.d.ts.map +1 -0
- package/dist/types/api-client.d.ts.map +1 -0
- package/dist/types/highlight.d.ts.map +1 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +39 -0
- package/package.json +17 -7
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAS,MAAM,YAAY,CAAC;AAmMzC,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;CAcnB,CAAC;AAGX,MAAM,MAAM,oBAAoB,GAAG,OAAO,gBAAgB,CAAC,MAAM,OAAO,gBAAgB,CAAC,CAAC;AAE1F,eAAO,MAAM,uBAAuB,SAC5B,IAAI,UACF,oBAAoB,SACrB,MAAM,wBACS,MAAM,qBAiF7B,CAAA"}
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { proboLogger } from './utils';
|
|
2
|
+
// Utility functions for enhanced Playwright interactions
|
|
3
|
+
async function handleNavigation(page, url, navigationTimeout = 5000, totalTimeout = 10000) {
|
|
4
|
+
let navigationCount = 0;
|
|
5
|
+
let lastNavigationTime = null;
|
|
6
|
+
const startTime = Date.now();
|
|
7
|
+
// Declare the function outside try block so it's accessible in finally
|
|
8
|
+
const onFrameNavigated = (frame) => {
|
|
9
|
+
if (frame === page.mainFrame()) {
|
|
10
|
+
navigationCount++;
|
|
11
|
+
lastNavigationTime = Date.now();
|
|
12
|
+
proboLogger.debug(`Navigation ${navigationCount} detected at ${lastNavigationTime}`);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
try {
|
|
16
|
+
// Setup navigation tracking
|
|
17
|
+
page.on('framenavigated', onFrameNavigated);
|
|
18
|
+
await page.goto(url, {
|
|
19
|
+
waitUntil: 'domcontentloaded',
|
|
20
|
+
timeout: totalTimeout
|
|
21
|
+
});
|
|
22
|
+
// Final wait only for DOM, not network
|
|
23
|
+
if (navigationCount > 0) {
|
|
24
|
+
proboLogger.debug(`Total navigations detected: ${navigationCount}`);
|
|
25
|
+
await page.waitForLoadState('domcontentloaded');
|
|
26
|
+
// Add scroll to bottom to trigger lazy loading
|
|
27
|
+
await scrollToBottomRight(page);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
page.removeListener('framenavigated', onFrameNavigated);
|
|
32
|
+
proboLogger.debug('Navigation listener removed');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function enhancedClick(page, selector, options = {}) {
|
|
36
|
+
// Track initial scroll position
|
|
37
|
+
let initScrollY = await page.evaluate(() => window.scrollY);
|
|
38
|
+
let initScrollX = await page.evaluate(() => window.scrollX);
|
|
39
|
+
// Perform the click
|
|
40
|
+
await page.click(selector, { noWaitAfter: false, ...options });
|
|
41
|
+
// Monitor for any scrolling that might occur due to the click
|
|
42
|
+
let scrollingDetected = false;
|
|
43
|
+
while (true) {
|
|
44
|
+
const currScrollX = await page.evaluate(() => window.scrollX);
|
|
45
|
+
const currScrollY = await page.evaluate(() => window.scrollY);
|
|
46
|
+
if (currScrollY !== initScrollY || currScrollX !== initScrollX) {
|
|
47
|
+
proboLogger.debug('Click detected scrolling, waiting 50ms for stabilization...');
|
|
48
|
+
scrollingDetected = true;
|
|
49
|
+
await page.waitForTimeout(50);
|
|
50
|
+
initScrollX = currScrollX;
|
|
51
|
+
initScrollY = currScrollY;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// If scrolling occurred, reset to top
|
|
58
|
+
if (scrollingDetected) {
|
|
59
|
+
await page.evaluate(() => window.scrollTo(0, 0));
|
|
60
|
+
await page.waitForTimeout(50); // Small delay between scrolls
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function selectDropdownOption(page, selector, value) {
|
|
64
|
+
// Get the element's tag name
|
|
65
|
+
const locator = page.locator(selector);
|
|
66
|
+
const tagName = await locator.evaluate(el => el.tagName.toLowerCase());
|
|
67
|
+
if (tagName === 'select') {
|
|
68
|
+
// Handle native select element
|
|
69
|
+
proboLogger.debug('selectDropdownOption: simple select tag');
|
|
70
|
+
await page.selectOption(selector, value);
|
|
71
|
+
}
|
|
72
|
+
else { // not a native select
|
|
73
|
+
try {
|
|
74
|
+
// Look for child <select> if exists
|
|
75
|
+
const listboxSelector = locator.locator('select');
|
|
76
|
+
const count = await listboxSelector.count();
|
|
77
|
+
if (count === 1) { // delegate to child <select>
|
|
78
|
+
proboLogger.debug('Identified child element with tag select');
|
|
79
|
+
await listboxSelector.selectOption(value);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// Click the original locator to expose the real listbox and options
|
|
83
|
+
proboLogger.debug('Clicking container element, looking for child element with role listbox');
|
|
84
|
+
await locator.click();
|
|
85
|
+
const listbox = locator.getByRole('listbox');
|
|
86
|
+
proboLogger.debug(`Clicking option ${value}`);
|
|
87
|
+
// Handle both string and array values
|
|
88
|
+
const values = Array.isArray(value) ? value : [value];
|
|
89
|
+
for (const val of values) {
|
|
90
|
+
const optionSelector = listbox
|
|
91
|
+
.getByRole('option')
|
|
92
|
+
.getByText(new RegExp(`^${val.toLowerCase()}$`, 'i'));
|
|
93
|
+
const optionCount = await optionSelector.count();
|
|
94
|
+
if (optionCount !== 1) {
|
|
95
|
+
throw new Error(`Found ${optionCount} option selectors for value: ${val}`);
|
|
96
|
+
}
|
|
97
|
+
await optionSelector.click();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
throw new Error(`Failed to select option '${value}' from custom dropdown: ${error}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function getElementValue(page, selector) {
|
|
107
|
+
// TODO: Implement getting all text contents
|
|
108
|
+
proboLogger.log("TODO: Implement getting all text contents");
|
|
109
|
+
return "TODO: Implement getting all text contents";
|
|
110
|
+
}
|
|
111
|
+
async function waitForMutationsToSettle(page) {
|
|
112
|
+
// TODO: Implement mutation observer
|
|
113
|
+
//proboLogger.log("TODO: Implement mutation observer");
|
|
114
|
+
await page.waitForTimeout(500);
|
|
115
|
+
}
|
|
116
|
+
async function scrollToBottomRight(page) {
|
|
117
|
+
proboLogger.debug("Scrolling to bottom of page...");
|
|
118
|
+
// Get initial page dimensions
|
|
119
|
+
let lastHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
120
|
+
let lastWidth = await page.evaluate(() => document.documentElement.scrollWidth);
|
|
121
|
+
while (true) {
|
|
122
|
+
let smoothingSteps = 0;
|
|
123
|
+
// Get current scroll positions and dimensions
|
|
124
|
+
const initScrollY = await page.evaluate(() => window.scrollY);
|
|
125
|
+
const initScrollX = await page.evaluate(() => window.scrollX);
|
|
126
|
+
const maxScrollY = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
127
|
+
const maxScrollX = await page.evaluate(() => document.documentElement.scrollWidth);
|
|
128
|
+
const maxClientY = await page.evaluate(() => document.documentElement.clientHeight);
|
|
129
|
+
const maxClientX = await page.evaluate(() => document.documentElement.clientWidth);
|
|
130
|
+
// Scroll both horizontally and vertically in steps
|
|
131
|
+
let currScrollX = initScrollX;
|
|
132
|
+
while (currScrollX <= (maxScrollX - maxClientX)) {
|
|
133
|
+
let currScrollY = initScrollY;
|
|
134
|
+
while (currScrollY <= (maxScrollY - maxClientY)) {
|
|
135
|
+
currScrollY += maxClientY;
|
|
136
|
+
await page.evaluate(({ x, y }) => window.scrollTo(x, y), { x: currScrollX, y: currScrollY });
|
|
137
|
+
await page.waitForTimeout(50); // Small delay between scrolls
|
|
138
|
+
smoothingSteps++;
|
|
139
|
+
}
|
|
140
|
+
currScrollX += maxClientX;
|
|
141
|
+
}
|
|
142
|
+
proboLogger.debug(`Performed ${smoothingSteps} smoothing steps while scrolling`);
|
|
143
|
+
// Check if content was lazy loaded (dimensions changed)
|
|
144
|
+
const newHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
145
|
+
const newWidth = await page.evaluate(() => document.documentElement.scrollWidth);
|
|
146
|
+
if (newHeight === lastHeight && newWidth === lastWidth) {
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
proboLogger.debug("Page dimensions updated, repeating smoothing steps with new dimensions");
|
|
150
|
+
lastHeight = newHeight;
|
|
151
|
+
lastWidth = newWidth;
|
|
152
|
+
}
|
|
153
|
+
// Reset scroll position to top left
|
|
154
|
+
await page.waitForTimeout(200);
|
|
155
|
+
await page.evaluate(() => window.scrollTo(0, 0));
|
|
156
|
+
await page.waitForTimeout(50);
|
|
157
|
+
}
|
|
158
|
+
export const PlaywrightAction = {
|
|
159
|
+
VISIT_BASE_URL: 'VISIT_BASE_URL',
|
|
160
|
+
VISIT_URL: 'VISIT_URL',
|
|
161
|
+
CLICK: 'CLICK',
|
|
162
|
+
FILL_IN: 'FILL_IN',
|
|
163
|
+
SELECT_DROPDOWN: 'SELECT_DROPDOWN',
|
|
164
|
+
SELECT_MULTIPLE_DROPDOWN: 'SELECT_MULTIPLE_DROPDOWN',
|
|
165
|
+
CHECK_CHECKBOX: 'CHECK_CHECKBOX',
|
|
166
|
+
SELECT_RADIO: 'SELECT_RADIO',
|
|
167
|
+
TOGGLE_SWITCH: 'TOGGLE_SWITCH',
|
|
168
|
+
PRESS_KEY: 'PRESS_KEY',
|
|
169
|
+
VALIDATE_EXACT_VALUE: 'VALIDATE_EXACT_VALUE',
|
|
170
|
+
VALIDATE_CONTAINS_VALUE: 'VALIDATE_CONTAINS_VALUE',
|
|
171
|
+
VALIDATE_URL: 'VALIDATE_URL'
|
|
172
|
+
};
|
|
173
|
+
export const executePlaywrightAction = async (page, action, value, element_css_selector) => {
|
|
174
|
+
proboLogger.info('Executing playwright action:', { action, value, element_css_selector });
|
|
175
|
+
try {
|
|
176
|
+
switch (action) {
|
|
177
|
+
case PlaywrightAction.VISIT_BASE_URL:
|
|
178
|
+
case PlaywrightAction.VISIT_URL:
|
|
179
|
+
await handleNavigation(page, value);
|
|
180
|
+
break;
|
|
181
|
+
case PlaywrightAction.CLICK:
|
|
182
|
+
await enhancedClick(page, element_css_selector);
|
|
183
|
+
break;
|
|
184
|
+
case PlaywrightAction.FILL_IN:
|
|
185
|
+
await enhancedClick(page, element_css_selector);
|
|
186
|
+
await page.fill(element_css_selector, value);
|
|
187
|
+
break;
|
|
188
|
+
case PlaywrightAction.SELECT_DROPDOWN:
|
|
189
|
+
await selectDropdownOption(page, element_css_selector, value);
|
|
190
|
+
break;
|
|
191
|
+
case PlaywrightAction.SELECT_MULTIPLE_DROPDOWN:
|
|
192
|
+
const options = value.split(',').map(opt => opt.trim());
|
|
193
|
+
await selectDropdownOption(page, element_css_selector, options);
|
|
194
|
+
break;
|
|
195
|
+
case PlaywrightAction.CHECK_CHECKBOX:
|
|
196
|
+
const checked = value.toLowerCase() === 'true';
|
|
197
|
+
await page.setChecked(element_css_selector, checked);
|
|
198
|
+
break;
|
|
199
|
+
case PlaywrightAction.SELECT_RADIO:
|
|
200
|
+
case PlaywrightAction.TOGGLE_SWITCH:
|
|
201
|
+
await enhancedClick(page, element_css_selector);
|
|
202
|
+
break;
|
|
203
|
+
case PlaywrightAction.PRESS_KEY:
|
|
204
|
+
await page.press(element_css_selector, value);
|
|
205
|
+
break;
|
|
206
|
+
case PlaywrightAction.VALIDATE_EXACT_VALUE:
|
|
207
|
+
const exactValue = await getElementValue(page, element_css_selector);
|
|
208
|
+
if (exactValue !== value) {
|
|
209
|
+
proboLogger.info(`Validation FAIL: expected ${value} but got ${exactValue}`);
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
proboLogger.info('Validation PASS');
|
|
213
|
+
return true;
|
|
214
|
+
case PlaywrightAction.VALIDATE_CONTAINS_VALUE:
|
|
215
|
+
const containsValue = await getElementValue(page, element_css_selector);
|
|
216
|
+
if (!containsValue.includes(value)) {
|
|
217
|
+
proboLogger.info(`Validation FAIL: expected ${value} to be contained in ${containsValue}`);
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
proboLogger.info('Validation PASS');
|
|
221
|
+
return true;
|
|
222
|
+
case PlaywrightAction.VALIDATE_URL:
|
|
223
|
+
const currentUrl = page.url();
|
|
224
|
+
if (currentUrl !== value) {
|
|
225
|
+
proboLogger.info(`Validation FAIL: expected url ${value} but got ${currentUrl}`);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
proboLogger.info('Validation PASS');
|
|
229
|
+
return true;
|
|
230
|
+
default:
|
|
231
|
+
throw new Error(`Unknown action: ${action}`);
|
|
232
|
+
}
|
|
233
|
+
// Wait for mutations to settle after any action
|
|
234
|
+
await waitForMutationsToSettle(page);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
proboLogger.error(`Failed to execute action ${action}:`, error);
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface Instruction {
|
|
2
|
+
what_to_do: string;
|
|
3
|
+
args: any;
|
|
4
|
+
result: any;
|
|
5
|
+
}
|
|
6
|
+
export interface CreateStepOptions {
|
|
7
|
+
stepIdFromServer: number;
|
|
8
|
+
stepPrompt: string;
|
|
9
|
+
initial_screenshot_url: string;
|
|
10
|
+
initial_html_content: string;
|
|
11
|
+
scenarioName: string;
|
|
12
|
+
use_cache: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare class ApiError extends Error {
|
|
15
|
+
status: number;
|
|
16
|
+
data?: any | undefined;
|
|
17
|
+
constructor(status: number, message: string, data?: any | undefined);
|
|
18
|
+
toString(): string;
|
|
19
|
+
}
|
|
20
|
+
export declare class ApiClient {
|
|
21
|
+
private apiUrl;
|
|
22
|
+
private token;
|
|
23
|
+
private maxRetries;
|
|
24
|
+
private initialBackoff;
|
|
25
|
+
constructor(apiUrl: string, token?: string, // Default to demo token
|
|
26
|
+
maxRetries?: number, initialBackoff?: number);
|
|
27
|
+
private handleResponse;
|
|
28
|
+
private getHeaders;
|
|
29
|
+
private withRetry;
|
|
30
|
+
createStep(options: CreateStepOptions): Promise<string>;
|
|
31
|
+
resolveNextInstruction(stepId: string, instruction: Instruction | null): Promise<any>;
|
|
32
|
+
uploadScreenshot(screenshot_bytes: Buffer): Promise<string>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=api-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,GAAG,CAAC;IACV,MAAM,EAAE,GAAG,CAAC;CACb;AAED,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,sBAAsB,EAAE,MAAM,CAAA;IAC9B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,qBAAa,QAAS,SAAQ,KAAK;IAExB,MAAM,EAAE,MAAM;IAEd,IAAI,CAAC,EAAE,GAAG;gBAFV,MAAM,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACR,IAAI,CAAC,EAAE,GAAG,YAAA;IASnB,QAAQ;CAGT;AAED,qBAAa,SAAS;IAElB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,cAAc;gBAHd,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,MAAmD,EAAG,wBAAwB;IACrF,UAAU,GAAE,MAAU,EACtB,cAAc,GAAE,MAAa;YAGzB,cAAc;IA+B5B,OAAO,CAAC,UAAU;YAWJ,SAAS;IAyBjB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBvD,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,IAAI;IAatE,gBAAgB,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAclE"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export class ApiError extends Error {
|
|
2
|
+
constructor(status, message, data) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.status = status;
|
|
5
|
+
this.data = data;
|
|
6
|
+
this.name = 'ApiError';
|
|
7
|
+
// Remove stack trace for cleaner error messages
|
|
8
|
+
this.stack = undefined;
|
|
9
|
+
}
|
|
10
|
+
toString() {
|
|
11
|
+
return `${this.message} (Status: ${this.status})`;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class ApiClient {
|
|
15
|
+
constructor(apiUrl, token = 'b31793f81a1f58b8a153c86a5fdf4df0e5179c51', // Default to demo token
|
|
16
|
+
maxRetries = 3, initialBackoff = 1000) {
|
|
17
|
+
this.apiUrl = apiUrl;
|
|
18
|
+
this.token = token;
|
|
19
|
+
this.maxRetries = maxRetries;
|
|
20
|
+
this.initialBackoff = initialBackoff;
|
|
21
|
+
}
|
|
22
|
+
async handleResponse(response) {
|
|
23
|
+
var _a;
|
|
24
|
+
try {
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
switch (response.status) {
|
|
28
|
+
case 401:
|
|
29
|
+
throw new ApiError(401, 'Unauthorized - Invalid or missing authentication token');
|
|
30
|
+
case 403:
|
|
31
|
+
throw new ApiError(403, 'Forbidden - You do not have permission to perform this action');
|
|
32
|
+
case 400:
|
|
33
|
+
throw new ApiError(400, 'Bad Request', data);
|
|
34
|
+
case 404:
|
|
35
|
+
throw new ApiError(404, 'Not Found', data);
|
|
36
|
+
case 500:
|
|
37
|
+
throw new ApiError(500, 'Internal Server Error', data);
|
|
38
|
+
default:
|
|
39
|
+
throw new ApiError(response.status, `API Error: ${data.error || 'Unknown error'}`, data);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
// Only throw the original error if it's not a network error
|
|
46
|
+
if (!((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('fetch failed'))) {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
throw new ApiError(0, 'Network error: fetch failed');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
getHeaders() {
|
|
53
|
+
const headers = {
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
};
|
|
56
|
+
// Always include token in headers now that we have a default
|
|
57
|
+
headers['Authorization'] = `Token ${this.token}`;
|
|
58
|
+
return headers;
|
|
59
|
+
}
|
|
60
|
+
async withRetry(operation) {
|
|
61
|
+
var _a;
|
|
62
|
+
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
|
63
|
+
try {
|
|
64
|
+
return await operation();
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// Only retry on network errors
|
|
68
|
+
if (!((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('Network error: fetch failed'))) {
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
// On last attempt, throw the error
|
|
72
|
+
if (attempt === this.maxRetries) {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
// Otherwise, wait and retry
|
|
76
|
+
console.log(`[probolib-LOG] Network error, retrying (${attempt}/${this.maxRetries}). Waiting ${this.initialBackoff * Math.pow(2, attempt - 1)}ms...`);
|
|
77
|
+
await new Promise(resolve => setTimeout(resolve, this.initialBackoff * Math.pow(2, attempt - 1)));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// TypeScript needs this, but it will never be reached
|
|
81
|
+
throw new Error('Unreachable');
|
|
82
|
+
}
|
|
83
|
+
async createStep(options) {
|
|
84
|
+
return this.withRetry(async () => {
|
|
85
|
+
const response = await fetch(`${this.apiUrl}/step-runners/`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: this.getHeaders(),
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
step_id: options.stepIdFromServer,
|
|
90
|
+
scenario_name: options.scenarioName,
|
|
91
|
+
step_prompt: options.stepPrompt,
|
|
92
|
+
initial_screenshot: options.initial_screenshot_url,
|
|
93
|
+
initial_html_content: options.initial_html_content,
|
|
94
|
+
use_cache: options.use_cache,
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
const data = await this.handleResponse(response);
|
|
98
|
+
return data.step.id;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async resolveNextInstruction(stepId, instruction) {
|
|
102
|
+
return this.withRetry(async () => {
|
|
103
|
+
const response = await fetch(`${this.apiUrl}/step-runners/${stepId}/run/`, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: this.getHeaders(),
|
|
106
|
+
body: JSON.stringify({ executed_instruction: instruction }),
|
|
107
|
+
});
|
|
108
|
+
const data = await this.handleResponse(response);
|
|
109
|
+
return data.instruction;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async uploadScreenshot(screenshot_bytes) {
|
|
113
|
+
return this.withRetry(async () => {
|
|
114
|
+
const response = await fetch(`${this.apiUrl}/upload-screenshots/`, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: {
|
|
117
|
+
'Authorization': `Token ${this.token}`
|
|
118
|
+
},
|
|
119
|
+
body: screenshot_bytes,
|
|
120
|
+
});
|
|
121
|
+
const data = await this.handleResponse(response);
|
|
122
|
+
return data.screenshot_url;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"highlight.d.ts","sourceRoot":"","sources":["../src/highlight.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,sCAAsC,CAAC;AAIlE,KAAK,cAAc,GAAG,OAAO,UAAU,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAGjE,OAAO,CAAC,MAAM,CAAC;IAEb,IAAI,eAAe,EAAE,MAAM,CAAC;IAE5B,UAAU,MAAM;QACd,SAAS,CAAC,EAAE;YACV,SAAS,EAAE;gBACT,OAAO,EAAE,CAAC,YAAY,EAAE,cAAc,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;gBAC1D,SAAS,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;aAC/B,CAAC;YACF,iBAAiB,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC;gBAAE,YAAY,EAAE,MAAM,CAAC;gBAAC,KAAK,EAAE,MAAM,CAAA;aAAE,CAAC,KAAK,IAAI,CAAC;YACtF,UAAU,EAAE,OAAO,UAAU,CAAC;SAC/B,CAAC;KACH;CACF;AAED,qBAAa,WAAW;IACV,OAAO,CAAC,iBAAiB;gBAAjB,iBAAiB,GAAE,OAAc;YAEvC,uBAAuB;IA6BxB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,cAAc;IAsBxD,mBAAmB,CAAC,IAAI,EAAE,IAAI;IAQ9B,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,oBAAoB,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM;CA0B9F;AAED,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ElementTag } from '@probolabs/highlighter/src/constants';
|
|
2
|
+
import { proboLogger } from './utils';
|
|
3
|
+
export class Highlighter {
|
|
4
|
+
constructor(enableConsoleLogs = true) {
|
|
5
|
+
this.enableConsoleLogs = enableConsoleLogs;
|
|
6
|
+
}
|
|
7
|
+
async ensureHighlighterScript(page, maxRetries = 3) {
|
|
8
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
9
|
+
try {
|
|
10
|
+
const scriptExists = await page.evaluate(`typeof window.ProboLabs?.highlight?.execute === 'function'`);
|
|
11
|
+
if (!scriptExists) {
|
|
12
|
+
proboLogger.debug('Injecting highlighter script...');
|
|
13
|
+
await page.evaluate(highlighterCode);
|
|
14
|
+
// Verify the script was injected correctly
|
|
15
|
+
const verified = await page.evaluate(`
|
|
16
|
+
//console.log('ProboLabs global:', window.ProboLabs);
|
|
17
|
+
typeof window.ProboLabs?.highlight?.execute === 'function'
|
|
18
|
+
`);
|
|
19
|
+
proboLogger.debug('Script injection verified:', verified);
|
|
20
|
+
}
|
|
21
|
+
return; // Success - exit the function
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (attempt === maxRetries - 1) {
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
proboLogger.debug(`Script injection attempt ${attempt + 1} failed, retrying after delay...`);
|
|
28
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async highlightElements(page, elementTag) {
|
|
33
|
+
proboLogger.debug('highlightElements called with:', elementTag);
|
|
34
|
+
await this.ensureHighlighterScript(page);
|
|
35
|
+
// Execute the highlight function and await its result
|
|
36
|
+
const result = await page.evaluate(async (tag) => {
|
|
37
|
+
var _a, _b;
|
|
38
|
+
//proboLogger.debug('Browser: Starting highlight execution with tag:', tag);
|
|
39
|
+
if (!((_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.highlight) === null || _b === void 0 ? void 0 : _b.execute)) {
|
|
40
|
+
console.error('Browser: ProboLabs.highlight.execute is not available!');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const elements = await window.ProboLabs.highlight.execute([tag]);
|
|
44
|
+
//proboLogger.debug('Browser: Found elements:', elements);
|
|
45
|
+
return elements;
|
|
46
|
+
}, elementTag);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
async unhighlightElements(page) {
|
|
50
|
+
proboLogger.debug('unhighlightElements called');
|
|
51
|
+
await this.ensureHighlighterScript(page);
|
|
52
|
+
await page.evaluate(() => {
|
|
53
|
+
var _a, _b;
|
|
54
|
+
(_b = (_a = window === null || window === void 0 ? void 0 : window.ProboLabs) === null || _a === void 0 ? void 0 : _a.highlight) === null || _b === void 0 ? void 0 : _b.unexecute();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async highlightElement(page, element_css_selector, element_index) {
|
|
58
|
+
await this.ensureHighlighterScript(page);
|
|
59
|
+
proboLogger.debug('Highlighting element with:', { element_css_selector, element_index });
|
|
60
|
+
await page.evaluate(({ css_selector, index }) => {
|
|
61
|
+
const proboLabs = window.ProboLabs;
|
|
62
|
+
if (!proboLabs) {
|
|
63
|
+
proboLogger.warn('ProboLabs not initialized');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Create ElementInfo object for the element
|
|
67
|
+
const elementInfo = {
|
|
68
|
+
css_selector: css_selector,
|
|
69
|
+
index: index
|
|
70
|
+
};
|
|
71
|
+
// Call highlightElements directly
|
|
72
|
+
proboLabs.highlightElements([elementInfo]);
|
|
73
|
+
}, {
|
|
74
|
+
css_selector: element_css_selector,
|
|
75
|
+
index: element_index
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export { ElementTag };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Page } from 'playwright';
|
|
2
|
+
import { ElementTag } from '@probolabs/highlighter/src/constants';
|
|
3
|
+
export { ElementTag } from '@probolabs/highlighter/src/constants';
|
|
4
|
+
|
|
5
|
+
type ElementTagType = typeof ElementTag[keyof typeof ElementTag];
|
|
6
|
+
declare global {
|
|
7
|
+
var highlighterCode: string;
|
|
8
|
+
interface Window {
|
|
9
|
+
ProboLabs?: {
|
|
10
|
+
highlight: {
|
|
11
|
+
execute: (elementTypes: ElementTagType[]) => Promise<any>;
|
|
12
|
+
unexecute: () => Promise<any>;
|
|
13
|
+
};
|
|
14
|
+
highlightElements: (elements: Array<{
|
|
15
|
+
css_selector: string;
|
|
16
|
+
index: string;
|
|
17
|
+
}>) => void;
|
|
18
|
+
ElementTag: typeof ElementTag;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
declare enum ProboLogLevel {
|
|
24
|
+
DEBUG = "DEBUG",
|
|
25
|
+
INFO = "INFO",
|
|
26
|
+
LOG = "LOG",
|
|
27
|
+
WARN = "WARN",
|
|
28
|
+
ERROR = "ERROR"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare const DEMO_TOKEN = "b31793f81a1f58b8a153c86a5fdf4df0e5179c51";
|
|
32
|
+
|
|
33
|
+
interface ProboConfig {
|
|
34
|
+
scenarioName: string;
|
|
35
|
+
token: string;
|
|
36
|
+
apiUrl: string;
|
|
37
|
+
enableConsoleLogs?: boolean;
|
|
38
|
+
debugLevel?: ProboLogLevel;
|
|
39
|
+
}
|
|
40
|
+
declare class Probo {
|
|
41
|
+
private highlighter;
|
|
42
|
+
private apiClient;
|
|
43
|
+
private readonly enableConsoleLogs;
|
|
44
|
+
private readonly scenarioName;
|
|
45
|
+
constructor({ scenarioName, token, apiUrl, enableConsoleLogs, debugLevel }: ProboConfig);
|
|
46
|
+
runStep(page: Page, stepPrompt: string, stepIdFromServer?: number | null, // Make optional
|
|
47
|
+
useCache?: boolean): Promise<boolean>;
|
|
48
|
+
private _handleStepCreation;
|
|
49
|
+
private setupConsoleLogs;
|
|
50
|
+
highlightElements(page: Page, elementTag: ElementTagType): Promise<any>;
|
|
51
|
+
unhighlightElements(page: Page): Promise<void>;
|
|
52
|
+
highlightElement(page: Page, element_css_selector: string, element_index: string): Promise<void>;
|
|
53
|
+
screenshot(page: Page): Promise<string>;
|
|
54
|
+
private _handlePerformAction;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { DEMO_TOKEN, Probo, ProboLogLevel };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,sCAAsC,CAAC;AAElE,OAAO,EAAe,cAAc,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,EAAgB,aAAa,EAAE,MAAM,SAAS,CAAC;AAEtD,eAAO,MAAM,UAAU,6CAA6C,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,CAAC;AACzB,UAAU,WAAW;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,UAAU,CAAC,EAAE,aAAa,CAAC;CAC5B;AAED,qBAAa,KAAK;IAChB,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAC1B,EACV,YAAY,EACZ,KAAK,EACL,MAAM,EACN,iBAAyB,EAC1B,EAAE,WAAW;IAQD,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,QAAQ,GAAE,OAAc,GAAI,OAAO,CAAC,OAAO,CAAC;YAgE7G,mBAAmB;IAgBjC,OAAO,CAAC,gBAAgB;IAUX,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,cAAc;IAIxD,mBAAmB,CAAC,IAAI,EAAE,IAAI;IAI9B,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,oBAAoB,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM;IAIhF,UAAU,CAAC,IAAI,EAAE,IAAI;YAQpB,oBAAoB;CAmCnC;AAGD,OAAO,EAAE,UAAU,EAAE,CAAC"}
|