@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/skyramp",
3
- "version": "1.2.21",
3
+ "version": "1.2.22",
4
4
  "description": "module for leveraging skyramp cli functionality",
5
5
  "scripts": {
6
6
  "lint": "eslint 'src/**/*.js' 'src/**/*.ts' --fix",
@@ -251,7 +251,7 @@ export declare class SkyrampClient {
251
251
  negativeScenario: boolean
252
252
  ): Promise<string[]>;
253
253
 
254
- sendRequest(options: SendRequestV2Options): Promise<void>;
254
+ sendRequest(options: SendRequestV2Options): Promise<ResponseV2>;
255
255
  deployDashboard(network: string): Promise<void>;
256
256
 
257
257
  generateRestTest(options: GenerateRestTestOptions): Promise<string>;
@@ -1,2 +1,5 @@
1
1
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
2
  export function newSkyrampPlaywrightPage(page: any): any;
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ export function expect(obj: any): any;
@@ -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} with ${this.execParam} ${this.execArgs}`)
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
- return this.execute().catch(error => {
148
- throw new Error("Potentially a hydration issue. Please add enough waitForTimeout()", error);
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("multiple locators identified")
156
- return this.execute();
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
- let couldBeHydration = false
162
- try {
163
- // check if last step has potential hydration
164
- if (this._previousLocator && this._previousLocator.locatorCount == 0) {
165
- couldBeHydration = true
166
- debug(`previous action ${this._previousLocator._locator} is potentially associated with hydration`);
167
- // wait for a short time to finish hydration
168
- await this._skyrampPage._page.waitForTimeout(1500);
169
- // then re-execute the previous locator
170
- await this._previousLocator.execute();
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
- } catch (error) {
189
- if (couldBeHydration && error.name == "TimeoutError") {
190
- return this.execute()
191
- .catch(error => {
192
- throw new Error(`Potentially a hydration issue. Please add enough waitForTimeout() here or before ${this._previousLocator._locator}`, error);
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
- } else {
195
- debug(`${this._locator} failed at first try. attempting again with some timeout`);
196
- // wait for a short time before retry
197
- await this._skyrampPage._page.waitForTimeout(1500);
198
- // then re-execute the current locator
199
- // highly likely non-existent locator
200
- return this.execute().catch(error => {
201
- 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`, error);
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) {