@skyramp/skyramp 1.2.21 → 1.2.22
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/package.json
CHANGED
|
@@ -251,7 +251,7 @@ export declare class SkyrampClient {
|
|
|
251
251
|
negativeScenario: boolean
|
|
252
252
|
): Promise<string[]>;
|
|
253
253
|
|
|
254
|
-
sendRequest(options: SendRequestV2Options): Promise<
|
|
254
|
+
sendRequest(options: SendRequestV2Options): Promise<ResponseV2>;
|
|
255
255
|
deployDashboard(network: string): Promise<void>;
|
|
256
256
|
|
|
257
257
|
generateRestTest(options: GenerateRestTestOptions): Promise<string>;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const { expect: playwrightExpect } = require('@playwright/test');
|
|
2
|
-
/*
|
|
3
2
|
const lib = require('../lib');
|
|
4
3
|
const koffi = require('koffi');
|
|
5
4
|
|
|
@@ -9,6 +8,9 @@ const responseType = koffi.struct({
|
|
|
9
8
|
});
|
|
10
9
|
|
|
11
10
|
const improvePlaywrightSelectorWrapper = lib.func('improvePlywrightSelectorWrapper', responseType, ['string']);
|
|
11
|
+
const hasJavascriptWrapper = lib.func('hasJavascript', 'bool', ['string']);
|
|
12
|
+
|
|
13
|
+
const defaultWaitForTimeout = 1500
|
|
12
14
|
|
|
13
15
|
async function improvePlaywrightSelectorWithLlm(originalSelector, errorMessage, domContext, pageTitle, pageURL) {
|
|
14
16
|
const requestData = {
|
|
@@ -16,7 +18,8 @@ async function improvePlaywrightSelectorWithLlm(originalSelector, errorMessage,
|
|
|
16
18
|
"error_message": errorMessage,
|
|
17
19
|
"dom_context": domContext,
|
|
18
20
|
"page_title": pageTitle,
|
|
19
|
-
"page_url": pageURL
|
|
21
|
+
"page_url": pageURL,
|
|
22
|
+
"language": "javascript"
|
|
20
23
|
};
|
|
21
24
|
const request = JSON.stringify(requestData);
|
|
22
25
|
|
|
@@ -39,7 +42,6 @@ async function improvePlaywrightSelectorWithLlm(originalSelector, errorMessage,
|
|
|
39
42
|
});
|
|
40
43
|
});
|
|
41
44
|
}
|
|
42
|
-
*/
|
|
43
45
|
|
|
44
46
|
function debug(...args) {
|
|
45
47
|
if (process.env.SKYRAMP_DEBUG == "true") {
|
|
@@ -47,6 +49,165 @@ function debug(...args) {
|
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
function shouldAttemptImprovement(errorMessage, errorName) {
|
|
53
|
+
const improvementKeywords = [
|
|
54
|
+
"timeout",
|
|
55
|
+
"not found",
|
|
56
|
+
"no element",
|
|
57
|
+
"not visible",
|
|
58
|
+
"not attached",
|
|
59
|
+
"selector resolved to hidden",
|
|
60
|
+
"element is not enabled",
|
|
61
|
+
"strict mode violation"
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
if (errorName != "") {
|
|
65
|
+
const playwrightErrorTypes = [
|
|
66
|
+
"TimeoutError",
|
|
67
|
+
"Error",
|
|
68
|
+
"LocatorAssertionError"
|
|
69
|
+
];
|
|
70
|
+
if (playwrightErrorTypes.some(e => e == errorName)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let errorLower = errorMessage.toLowerCase();
|
|
76
|
+
return improvementKeywords.some(keyword => errorLower.includes(keyword));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// for debugging only
|
|
80
|
+
/*
|
|
81
|
+
const sampleSuggestions = [
|
|
82
|
+
{
|
|
83
|
+
selector: "page.getByText('K', { exact: true})",
|
|
84
|
+
confidence: 0.9,
|
|
85
|
+
reasoning: "The 'L' appears to be within a user profile button that shows 'K' as the user initial, using semantic role for better reliability"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
selector: "page.locator('#onyx-user-dropdown')",
|
|
89
|
+
confidence: 0.85,
|
|
90
|
+
reasoning: 'Targets the specific user dropdown container with unique ID that contains the user initial'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
selector: "page.locator('.bg-background-900').filter({ hasText: 'K' })",
|
|
94
|
+
confidence: 0.8,
|
|
95
|
+
reasoning: 'Targets the specific styled element containing the user initial with background class'
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
selector: "page.locator('div').filter({ hasText: /^K$/ }).nth(0)",
|
|
99
|
+
confidence: 0.75,
|
|
100
|
+
reasoning: "Uses regex to find exact 'K' text match and takes first occurrence"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
selector: "page.getByText('kolja').locator('..').getByText('K', { exact: true })",
|
|
104
|
+
confidence: 0.7,
|
|
105
|
+
reasoning: "Navigates to parent of username text and finds the exact 'K' text within that context"
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
function parseErrorStack(ori) {
|
|
111
|
+
const stackLines = ori.split('\n');
|
|
112
|
+
let prev = ""
|
|
113
|
+
for (let idx = stackLines.length - 1; idx >= 0 ; idx --) {
|
|
114
|
+
// this could be improved
|
|
115
|
+
if (stackLines[idx].includes("@skyramp/skyramp")) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
prev = stackLines[idx];
|
|
119
|
+
}
|
|
120
|
+
return prev
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function retryWithLLM(skyrampLocator, error) {
|
|
124
|
+
// when API_KEY is not given, we return right away
|
|
125
|
+
if (!process.env.API_KEY) {
|
|
126
|
+
throw error
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let errorMessage = error.toString();
|
|
130
|
+
let errorType = typeof error;
|
|
131
|
+
|
|
132
|
+
if (!(skyrampLocator instanceof SkyrampPlaywrightLocator)) {
|
|
133
|
+
throw error
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let locatorStr = skyrampLocator._locator.toString();
|
|
137
|
+
|
|
138
|
+
if (!shouldAttemptImprovement(errorMessage, errorType)) {
|
|
139
|
+
debug("cannot improve", error.name);
|
|
140
|
+
throw error
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let pageTitle = await skyrampLocator.page().title();
|
|
144
|
+
let pageUrl = skyrampLocator.page().url();
|
|
145
|
+
|
|
146
|
+
debug("retry with LLM", skyrampLocator._locator);
|
|
147
|
+
|
|
148
|
+
let newMsg = `${errorMessage} (while using selector: ${locatorStr})`;
|
|
149
|
+
|
|
150
|
+
let pageContent = await skyrampLocator.page().content();
|
|
151
|
+
|
|
152
|
+
// for debugging only
|
|
153
|
+
// let suggestions = sampleSuggestions;
|
|
154
|
+
|
|
155
|
+
var suggestions;
|
|
156
|
+
try {
|
|
157
|
+
suggestions = await improvePlaywrightSelectorWithLlm(
|
|
158
|
+
locatorStr,
|
|
159
|
+
newMsg,
|
|
160
|
+
pageContent,
|
|
161
|
+
pageTitle,
|
|
162
|
+
pageUrl
|
|
163
|
+
);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
debug("failed to consult with llm", error)
|
|
166
|
+
throw error
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (suggestions == null || suggestions.length == 0) {
|
|
170
|
+
debug("No LLM suggestoins available, failing with original error");
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
suggestions.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0))
|
|
175
|
+
debug(suggestions)
|
|
176
|
+
|
|
177
|
+
for (let suggestion of suggestions) {
|
|
178
|
+
debug(`try ${suggestion.selector} instead of ${skyrampLocator._locator}`);
|
|
179
|
+
try {
|
|
180
|
+
const newLocator = skyrampLocator.createLocatorFromString(suggestion.selector);
|
|
181
|
+
if (newLocator == null) {
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const locatorCount = await newLocator.count();
|
|
186
|
+
|
|
187
|
+
debug(` trying new Locator ${newLocator}, count = ${locatorCount}`);
|
|
188
|
+
|
|
189
|
+
if (locatorCount == 1) {
|
|
190
|
+
const func = newLocator[skyrampLocator.execFname];
|
|
191
|
+
return func.call(newLocator, skyrampLocator.execParam, skyrampLocator.execArgs).then(result => {
|
|
192
|
+
console.log(`✅ SUCCESS! Used selector: ${newLocator} instead of ${skyrampLocator._locator}`);
|
|
193
|
+
console.log(` ${parseErrorStack(error.stack)}`);
|
|
194
|
+
|
|
195
|
+
skyrampLocator.llmLocator = newLocator;
|
|
196
|
+
skyrampLocator.locatorCount = 1;
|
|
197
|
+
skyrampLocator._skyrampPage.addLLMChoices(skyrampLocator._locator, newLocator, error.stack);
|
|
198
|
+
return result;
|
|
199
|
+
}).catch(error => {
|
|
200
|
+
throw new Error(`retrying with LLM failed at ${skyrampLocator._locator} replacing ${newLocator}`, error);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
throw new Error(`failed to find a working locator from LLM's suggestions`, error);
|
|
209
|
+
}
|
|
210
|
+
|
|
50
211
|
class SkyrampPlaywrightLocator {
|
|
51
212
|
constructor(skyrampPage, locator, prevLocator, args, options, hydration) {
|
|
52
213
|
this._skyrampPage = skyrampPage
|
|
@@ -84,49 +245,29 @@ class SkyrampPlaywrightLocator {
|
|
|
84
245
|
return this._previousLocator && this._previousLocator.isHydration()
|
|
85
246
|
}
|
|
86
247
|
|
|
87
|
-
shouldAttemptImprovement(errorMessage, errorType) {
|
|
88
|
-
const improvementKeywords = [
|
|
89
|
-
"timeout",
|
|
90
|
-
"not found",
|
|
91
|
-
"no element",
|
|
92
|
-
"not visible",
|
|
93
|
-
"not attached",
|
|
94
|
-
"selector resolved to hidden",
|
|
95
|
-
"element is not enabled",
|
|
96
|
-
];
|
|
97
|
-
|
|
98
|
-
if (errorType != null) {
|
|
99
|
-
const playwrightErrorTypes = [
|
|
100
|
-
"TimeoutError",
|
|
101
|
-
"Error",
|
|
102
|
-
"LocatorAssertionError"
|
|
103
|
-
];
|
|
104
|
-
if (playwrightErrorTypes.some(errType => errorType.name.endsWith(errType))) {
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
let errorLower = errorMessage.toLowerCase();
|
|
110
|
-
return improvementKeywords.some(keyword => errorLower.includes(keyword));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
isSelectorMethod(methodName) {
|
|
114
|
-
// Check if a method uses selectors that can be improved.
|
|
115
|
-
const selectorMethods = [
|
|
116
|
-
'click', 'fill', 'type', 'press', 'check', 'uncheck', 'select_option',
|
|
117
|
-
'hover', 'focus', 'blur', 'scroll_into_view_if_needed', 'screenshot',
|
|
118
|
-
'text_content', 'inner_text', 'inner_html', 'get_attribute', 'is_visible',
|
|
119
|
-
'is_enabled', 'is_checked', 'is_disabled', 'is_editable', 'is_hidden'
|
|
120
|
-
]
|
|
121
|
-
return selectorMethods.includes(methodName)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
248
|
async execute() {
|
|
125
|
-
debug(`execute ${ this._locator}.${this.execFname}
|
|
249
|
+
debug(`execute ${ this._locator}.${this.execFname} ${this.execParam ?? ''} ${this.execArgs ?? ''}`)
|
|
126
250
|
const func = this._locator[this.execFname];
|
|
127
251
|
return func.call(this._locator, this.execParam, this.execArgs);
|
|
128
252
|
}
|
|
129
253
|
|
|
254
|
+
generateLLMErrors() {
|
|
255
|
+
let ret = "List of dynamic selectors that could be relevant to error:\n"
|
|
256
|
+
|
|
257
|
+
let choices = this._skyrampPage.getLLMChoices();
|
|
258
|
+
if (choices ==undefined || choices.length == 0) {
|
|
259
|
+
return ""
|
|
260
|
+
}
|
|
261
|
+
for (let i = 0 ; i < choices.length; i ++) {
|
|
262
|
+
const choice = choices[i];
|
|
263
|
+
ret += `${ i+1 }. original locator: ${choice.original}\n`
|
|
264
|
+
ret += ` selected locator: ${choice.new}\n`
|
|
265
|
+
ret += ` ${choice.at}\n`
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return ret;
|
|
269
|
+
}
|
|
270
|
+
|
|
130
271
|
// this is the function that does smart selector retry
|
|
131
272
|
async SmartRetryWithFallback(fname, param, ...args) {
|
|
132
273
|
this.execFname = fname;
|
|
@@ -137,6 +278,9 @@ class SkyrampPlaywrightLocator {
|
|
|
137
278
|
debug(`handling ${ this._locator }.${ fname }, count = ${ locatorCount }`)
|
|
138
279
|
this.locatorCount = locatorCount
|
|
139
280
|
|
|
281
|
+
let currentUrl = this._skyrampPage._page.url();
|
|
282
|
+
debug(`current url = ${currentUrl}`);
|
|
283
|
+
|
|
140
284
|
// if locator exists, DOM is available
|
|
141
285
|
if (locatorCount == 1) {
|
|
142
286
|
// we try action, this will most likely succeed
|
|
@@ -144,65 +288,217 @@ class SkyrampPlaywrightLocator {
|
|
|
144
288
|
return this.execute()
|
|
145
289
|
.catch(error => {
|
|
146
290
|
if (error.name == "TimeoutError") {
|
|
147
|
-
|
|
148
|
-
|
|
291
|
+
debug(`locator ${this._locator} exists, but execution failed, wait a bit and retry`)
|
|
292
|
+
this.wait(defaultWaitForTimeout)
|
|
293
|
+
|
|
294
|
+
return this.execute().catch(newError => {
|
|
295
|
+
debug(`second attempt on ${this._locator} failed`)
|
|
296
|
+
debug(newError)
|
|
297
|
+
if (this._skyrampPage.hasLLMChoices()) {
|
|
298
|
+
newError.message += this.generateLLMErrors();
|
|
299
|
+
throw newError;
|
|
300
|
+
} else {
|
|
301
|
+
throw new Error("Potentially a hydration issue. Please add enough waitForTimeout()", error);
|
|
302
|
+
}
|
|
149
303
|
});
|
|
150
304
|
}
|
|
151
305
|
throw error;
|
|
152
306
|
});
|
|
153
307
|
} else if (locatorCount > 0) {
|
|
154
308
|
// TODO
|
|
155
|
-
debug(
|
|
156
|
-
|
|
309
|
+
debug(`multiple ${locatorCount} locators identified`)
|
|
310
|
+
|
|
311
|
+
if (locatorCount > 5) {
|
|
312
|
+
throw new Error(`${locatorCount} locators detected for ${this._locator}. Please add "data-testid" attribute for a more stable locator`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return this.execute().then(result => {
|
|
316
|
+
return result;
|
|
317
|
+
}).catch(error => {
|
|
318
|
+
if (!process.env.API_KEY) {
|
|
319
|
+
throw new Error(`${locatorCount} locators detected for ${this._locator}. Please add "data-testid" attribute for a more stable locator`, error)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
console.log(`${locatorCount} locators found for ${this._locator}`)
|
|
323
|
+
// this is the palce where we consult with LLM
|
|
324
|
+
return retryWithLLM(this, error).then(result => {
|
|
325
|
+
this.multiLocatorLLM = true;
|
|
326
|
+
return result;
|
|
327
|
+
}).catch(newError => {
|
|
328
|
+
if (this._skyrampPage.hasLLMChoices()) {
|
|
329
|
+
newError.message += this.generateLLMErrors();
|
|
330
|
+
throw newError;
|
|
331
|
+
}
|
|
332
|
+
throw new Error(`failed to find a good alternative for ${this._locator}. Please add "data-testid" attribute for a more stable locator`, newError);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
157
335
|
} else {
|
|
158
336
|
// if locator does not exist, we need to consider two cases
|
|
159
337
|
// one is if any actions that required hydration did not work
|
|
160
338
|
// second, if locator id is not correct
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
339
|
+
// check if last step has potential hydration
|
|
340
|
+
if (this._previousLocator && this._previousLocator.locatorCount == 0) {
|
|
341
|
+
debug(`previous action ${this._previousLocator._locator} is potentially associated with hydration`);
|
|
342
|
+
// wait for a short time to finish hydration
|
|
343
|
+
await this.wait(defaultWaitForTimeout);
|
|
344
|
+
|
|
345
|
+
const previousCount = await this._previousLocator.count();
|
|
346
|
+
debug(` re-execute the previous one ${this._previousLocator._locator}, previous Locator count = ${previousCount}`);
|
|
347
|
+
|
|
348
|
+
// re-execute the previous locator
|
|
349
|
+
await this._previousLocator.execute().catch(() => {
|
|
350
|
+
// log the failure but continues to the current one
|
|
351
|
+
debug(`failed to execute previous locator ${this._previousLocator._locator} again`);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
try {
|
|
172
355
|
// then execute the current one
|
|
173
|
-
return this.execute();
|
|
356
|
+
return await this.execute();
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (error.name == "TimeoutError") {
|
|
359
|
+
debug(`${this._locator} failed at first try. attempting again with some timeout`);
|
|
360
|
+
// wait for some time and re execute
|
|
361
|
+
await this.wait(defaultWaitForTimeout);
|
|
362
|
+
|
|
363
|
+
return this.execute().catch(newError => {
|
|
364
|
+
if (!process.env.API_KEY) {
|
|
365
|
+
throw new Error(`Cannot find locator ${this._locator} and likely a hydration issue on ${this._previousLocator._locator}. Please add enough waitForTimeout()`, newError);
|
|
366
|
+
}
|
|
367
|
+
// retry with llm
|
|
368
|
+
return retryWithLLM(this, newError).then(result => {
|
|
369
|
+
this.singleLocatorLLM = true;
|
|
370
|
+
return result;
|
|
371
|
+
}).catch(newError2 => {
|
|
372
|
+
if (this._skyrampPage.hasLLMChoices()) {
|
|
373
|
+
newError2.message += this.generateLLMErrors();
|
|
374
|
+
throw newError2;
|
|
375
|
+
}
|
|
376
|
+
throw new Error(`Failed to find a good alternative of ${this._locator}. Please add "data-testid" attribute for a more stable locator`, newError2);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
if (this._previousLocator && this._previousLocator.multiLocatorLLM) {
|
|
384
|
+
debug(`${this._locator} locator count is zero, but previous locator was selected by LLM from many candidates`);
|
|
385
|
+
} else if (this._previousLocator && this._previousLocator.singleLocatorLLM) {
|
|
386
|
+
debug(`${this._locator} locator count is zero, but previous locator was selected by LLM from zero candidate`);
|
|
174
387
|
} else {
|
|
175
388
|
debug(`${this._locator} locator count is zero, but previous locator seems not related to hydration`);
|
|
176
|
-
// previous action may not be associated with hydration
|
|
177
|
-
// then we just try current locator. could be a locator with a wrong id
|
|
178
|
-
// wait for a short time just in case
|
|
179
|
-
await this._skyrampPage._page.waitForTimeout(1500);
|
|
180
|
-
|
|
181
|
-
return await this.execute().then(result => {
|
|
182
|
-
this.locatorCount = 1;
|
|
183
|
-
return result
|
|
184
|
-
}).catch(error => {
|
|
185
|
-
throw error;
|
|
186
|
-
});
|
|
187
389
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
390
|
+
// previous action may not be associated with hydration
|
|
391
|
+
// then we just try current locator. could be a locator with a wrong id
|
|
392
|
+
// wait for a short time just in case
|
|
393
|
+
await this.wait(defaultWaitForTimeout);
|
|
394
|
+
|
|
395
|
+
this.locatorCount = await this._locator.count();
|
|
396
|
+
debug(` after waiting locator count = ${this.locatorCount}, url = ${this._skyrampPage._page.url()}`);
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
return await this.execute();
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (error.name == "TimeoutError") {
|
|
402
|
+
debug(`${this._locator} failed at first try. attempting again with some timeout`);
|
|
403
|
+
await this.wait(defaultWaitForTimeout);
|
|
404
|
+
|
|
405
|
+
return this.execute().catch(newError => {
|
|
406
|
+
if (newError.name == "TimeoutError") {
|
|
407
|
+
if (!process.env.API_KEY) {
|
|
408
|
+
throw new Error(`Cannot find locator ${this._locator} and most likely not a hydration issue. Please add "data-testid" attribute for a more stable locator`, newError);
|
|
409
|
+
}
|
|
410
|
+
// retry with llm
|
|
411
|
+
return retryWithLLM(this, newError).then(result => {
|
|
412
|
+
this.singleLocatorLLM = true;
|
|
413
|
+
return result;
|
|
414
|
+
}).catch(newError2 => {
|
|
415
|
+
if (this._skyrampPage.hasLLMChoices()) {
|
|
416
|
+
newError2.message += this.generateLLMErrors();
|
|
417
|
+
throw newError2;
|
|
418
|
+
}
|
|
419
|
+
throw new Error(`Failed to find a good alternative of ${this._locator}. Please add "data-testid" attribute for a more stable locator`, newError2);
|
|
420
|
+
});
|
|
421
|
+
} else {
|
|
422
|
+
throw newError;
|
|
423
|
+
}
|
|
193
424
|
});
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
425
|
+
}
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
parseFunctionChain(chainStr) {
|
|
433
|
+
// Remove any object prefix like "page."
|
|
434
|
+
chainStr = chainStr.replace(/^[a-zA-Z_][a-zA-Z0-9_]*\./, '');
|
|
435
|
+
|
|
436
|
+
// Match function calls: funcName(args)
|
|
437
|
+
const regex = /([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\)/g;
|
|
438
|
+
let result = [];
|
|
439
|
+
let match;
|
|
440
|
+
while ((match = regex.exec(chainStr)) !== null) {
|
|
441
|
+
const funcName = match[1];
|
|
442
|
+
const argsStr = match[2].trim();
|
|
443
|
+
let args = [];
|
|
444
|
+
if (argsStr.length > 0) {
|
|
445
|
+
// Split arguments by comma, but keep objects and strings together
|
|
446
|
+
// This is a simple approach, for more complex cases use a parser
|
|
447
|
+
let argMatches = [];
|
|
448
|
+
let depth = 0, current = '', inString = false, stringChar = '';
|
|
449
|
+
for (let i = 0; i < argsStr.length; i++) {
|
|
450
|
+
const c = argsStr[i];
|
|
451
|
+
if ((c === "'" || c === '"') && !inString) {
|
|
452
|
+
inString = true;
|
|
453
|
+
stringChar = c;
|
|
454
|
+
current += c;
|
|
455
|
+
} else if (inString && c === stringChar) {
|
|
456
|
+
inString = false;
|
|
457
|
+
current += c;
|
|
458
|
+
} else if (!inString && (c === '{' || c === '[')) {
|
|
459
|
+
depth++;
|
|
460
|
+
current += c;
|
|
461
|
+
} else if (!inString && (c === '}' || c === ']')) {
|
|
462
|
+
depth--;
|
|
463
|
+
current += c;
|
|
464
|
+
} else if (!inString && c === ',' && depth === 0) {
|
|
465
|
+
argMatches.push(current.trim());
|
|
466
|
+
current = '';
|
|
467
|
+
} else {
|
|
468
|
+
current += c;
|
|
469
|
+
}
|
|
203
470
|
}
|
|
471
|
+
if (current.trim().length > 0) {
|
|
472
|
+
argMatches.push(current.trim());
|
|
473
|
+
}
|
|
474
|
+
args = argMatches;
|
|
475
|
+
}
|
|
476
|
+
result.push({ function: funcName, arguments: args });
|
|
477
|
+
}
|
|
478
|
+
return result;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
createLocatorFromString(suggested) {
|
|
482
|
+
suggested = suggested.trim();
|
|
483
|
+
if (suggested.startsWith("page.")) {
|
|
484
|
+
suggested = suggested.substring(5);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
let chains = this.parseFunctionChain(suggested);
|
|
488
|
+
|
|
489
|
+
let cur = this.page();
|
|
490
|
+
// find locator per function chain
|
|
491
|
+
for (let i = 0; i < chains.length; i ++) {
|
|
492
|
+
const chain = chains[i];
|
|
493
|
+
let f = cur[chain.function];
|
|
494
|
+
let args = [];
|
|
495
|
+
for (const a of chain.arguments) {
|
|
496
|
+
let arg = Function('"use strict";return (' + a + ')')();
|
|
497
|
+
args.push(arg);
|
|
204
498
|
}
|
|
499
|
+
cur = f.call(cur, ...args);
|
|
205
500
|
}
|
|
501
|
+
return cur;
|
|
206
502
|
}
|
|
207
503
|
|
|
208
504
|
async click(...args) {
|
|
@@ -315,11 +611,17 @@ class SkyrampPlaywrightLocator {
|
|
|
315
611
|
page() {
|
|
316
612
|
return this._skyrampPage._page;
|
|
317
613
|
}
|
|
614
|
+
|
|
615
|
+
async wait(t) {
|
|
616
|
+
debug(`wait for ${t}`);
|
|
617
|
+
return this._skyrampPage._page.waitForTimeout(t);
|
|
618
|
+
}
|
|
318
619
|
}
|
|
319
620
|
|
|
320
621
|
class SkyrampPlaywrightPage {
|
|
321
622
|
constructor(page) {
|
|
322
623
|
this._page = page;
|
|
624
|
+
this._curUrl = "";
|
|
323
625
|
return new Proxy(this, {
|
|
324
626
|
// The `get` trap is the key to forwarding.
|
|
325
627
|
// This will foraward any methods not implemented in this struct
|
|
@@ -344,6 +646,10 @@ class SkyrampPlaywrightPage {
|
|
|
344
646
|
});
|
|
345
647
|
}
|
|
346
648
|
|
|
649
|
+
getCurUrl() {
|
|
650
|
+
return this._curUrl;
|
|
651
|
+
}
|
|
652
|
+
|
|
347
653
|
pushLocator(locator) {
|
|
348
654
|
if (this.locators == undefined ) {
|
|
349
655
|
this.locators = [];
|
|
@@ -372,6 +678,7 @@ class SkyrampPlaywrightPage {
|
|
|
372
678
|
debug(`handling ${originalLocator}`);
|
|
373
679
|
}
|
|
374
680
|
*/
|
|
681
|
+
|
|
375
682
|
let newLocator = new SkyrampPlaywrightLocator(this, originalLocator, prevLocator, [param], options, hydration);
|
|
376
683
|
this.pushLocator(newLocator)
|
|
377
684
|
return newLocator
|
|
@@ -416,6 +723,43 @@ class SkyrampPlaywrightPage {
|
|
|
416
723
|
const originalLocator = this._page.getByAltText(alt, options);
|
|
417
724
|
return this.newSkyrampPlaywrightLocator(originalLocator, alt, options);
|
|
418
725
|
}
|
|
726
|
+
|
|
727
|
+
async goto(url, options) {
|
|
728
|
+
const result = await this._page.goto(url, options);
|
|
729
|
+
const content = await this._page.content();
|
|
730
|
+
if (hasJavascriptWrapper(content)) {
|
|
731
|
+
debug(`javascript download detected when visiting ${this._page.url()}`);
|
|
732
|
+
debug(` wait for sometime for potential hydration`);
|
|
733
|
+
await this._page.waitForTimeout(defaultWaitForTimeout);
|
|
734
|
+
} else {
|
|
735
|
+
debug(`javascript not detected when visiting ${this._page.url()}`);
|
|
736
|
+
}
|
|
737
|
+
this._curUrl = this._page.url();
|
|
738
|
+
return result;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
addLLMChoices(originalLocator, newLocator, stack) {
|
|
742
|
+
if (this.llmChoices == undefined) {
|
|
743
|
+
this.llmChoices = []
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
this.llmChoices.push({
|
|
747
|
+
"original": originalLocator.toString(),
|
|
748
|
+
"new": newLocator.toString(),
|
|
749
|
+
"at": parseErrorStack(stack),
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
getLLMChoices() {
|
|
754
|
+
return this.llmChoices
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
hasLLMChoices() {
|
|
758
|
+
if (this.llmChoices == undefined || this.llmChoices.length == 0) {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
419
763
|
}
|
|
420
764
|
|
|
421
765
|
function newSkyrampPlaywrightPage(page) {
|