@skyramp/skyramp 1.2.23 → 1.2.25

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.23",
3
+ "version": "1.2.25",
4
4
  "description": "module for leveraging skyramp cli functionality",
5
5
  "scripts": {
6
6
  "lint": "eslint 'src/**/*.js' 'src/**/*.ts' --fix",
@@ -267,4 +267,11 @@ export declare class SkyrampClient {
267
267
  scenario: AsyncScenario | AsyncScenario[],
268
268
  options?: SendScenarioOptions
269
269
  ): Promise<AsyncTestStatus>;
270
+
271
+ /**
272
+ * Fetches OTP from Skyramp email
273
+ */
274
+ fetchLatestOtp(
275
+ email: string
276
+ ): string;
270
277
  }
@@ -3,6 +3,7 @@ const koffi = require('koffi');
3
3
  const TrafficConfig = require('./TrafficConfig');
4
4
  const RequestV2 = require('./RequestV2');
5
5
  const ResponseV2 = require('./ResponseV2');
6
+ const utils = require('../utils');
6
7
 
7
8
  const workerInfoType = koffi.struct({
8
9
  container_name: 'char*',
@@ -99,6 +100,7 @@ class SkyrampClient {
99
100
  */
100
101
  constructor(kubeconfigPathOrOptions, clusterName, context, userToken, directory = process.cwd()) {
101
102
  this.local_image = false;
103
+ this.timestamp = Date.now();
102
104
  if (typeof kubeconfigPathOrOptions === 'object') {
103
105
  const options = kubeconfigPathOrOptions;
104
106
  this.workerNamespaces = [];
@@ -966,6 +968,38 @@ class SkyrampClient {
966
968
  );
967
969
  });
968
970
  }
971
+
972
+ async fetchLatestOtp(email) {
973
+ var latestGetResponse
974
+ // we use the time that this client was instantiated
975
+ let timestamp = this.timestamp;
976
+
977
+ if (!email.includes("@otp.skyramp.dev")) {
978
+ throw new Error("non-Skyramp email is not supported")
979
+ }
980
+
981
+ const username = email.replace("@otp.skyramp.dev", "")
982
+
983
+ for (let i = 0; i < 10; i ++) {
984
+ // Execute Request
985
+ latestGetResponse = await this.sendRequest({
986
+ url: "https://tokenize.skyramp.dev",
987
+ path: `/msg/${username}/latest`,
988
+ method: "GET"
989
+ });
990
+
991
+ if (latestGetResponse.statusCode != 200) {
992
+ await new Promise(resolve => setTimeout(resolve, 1000));
993
+ continue
994
+ }
995
+
996
+ let codeTime = new Date(utils.getValue(latestGetResponse.responseBody, "timestamp"));
997
+ if (codeTime > timestamp) {
998
+ return utils.getValue(latestGetResponse.responseBody, "otp")
999
+ }
1000
+ await new Promise(resolve => setTimeout(resolve, 1000));
1001
+ }
1002
+ }
969
1003
  }
970
1004
 
971
1005
  module.exports = SkyrampClient;
@@ -143,8 +143,6 @@ async function retryWithLLM(skyrampLocator, error) {
143
143
  let pageTitle = await skyrampLocator.page().title();
144
144
  let pageUrl = skyrampLocator.page().url();
145
145
 
146
- debug("retry with LLM", skyrampLocator._locator);
147
-
148
146
  let newMsg = `${errorMessage} (while using selector: ${locatorStr})`;
149
147
 
150
148
  let pageContent = await skyrampLocator.page().content();
@@ -167,7 +165,7 @@ async function retryWithLLM(skyrampLocator, error) {
167
165
  }
168
166
 
169
167
  if (suggestions == null || suggestions.length == 0) {
170
- debug("No LLM suggestoins available, failing with original error");
168
+ debug("No LLM suggestions available, failing with original error");
171
169
  throw error;
172
170
  }
173
171
 
@@ -175,7 +173,6 @@ async function retryWithLLM(skyrampLocator, error) {
175
173
  debug(suggestions)
176
174
 
177
175
  for (let suggestion of suggestions) {
178
- debug(`try ${suggestion.selector} instead of ${skyrampLocator._locator}`);
179
176
  try {
180
177
  const newLocator = skyrampLocator.createLocatorFromString(suggestion.selector);
181
178
  if (newLocator == null) {
@@ -184,7 +181,7 @@ async function retryWithLLM(skyrampLocator, error) {
184
181
 
185
182
  const locatorCount = await newLocator.count();
186
183
 
187
- debug(` trying new Locator ${newLocator}, count = ${locatorCount}`);
184
+ debug(`trying new Locator ${newLocator} instead of ${skyrampLocator._locator}, count = ${locatorCount}`);
188
185
 
189
186
  if (locatorCount == 1) {
190
187
  const func = newLocator[skyrampLocator.execFname];
@@ -197,7 +194,8 @@ async function retryWithLLM(skyrampLocator, error) {
197
194
  skyrampLocator._skyrampPage.addLLMChoices(skyrampLocator._locator, newLocator, error.stack);
198
195
  return result;
199
196
  }).catch(error => {
200
- throw new Error(`retrying with LLM failed at ${skyrampLocator._locator} replacing ${newLocator}`, error);
197
+ // if it fails, move to the next one
198
+ debug(`retrying with LLM failed at ${skyrampLocator._locator} replaced by {newLocator}`, error.name);
201
199
  });
202
200
  }
203
201
  } catch {
@@ -205,17 +203,18 @@ async function retryWithLLM(skyrampLocator, error) {
205
203
  }
206
204
  }
207
205
 
208
- throw new Error(`failed to find a working locator from LLM's suggestions`, error);
206
+ error.message = `Failed to find a good alternative for ${skyrampLocator._locator} with LLM.\n` +
207
+ `Please add "data-testid" attribute for a more stable locator\n` + error.message;
208
+ throw error;
209
209
  }
210
210
 
211
211
  class SkyrampPlaywrightLocator {
212
- constructor(skyrampPage, locator, prevLocator, args, options, hydration) {
212
+ constructor(skyrampPage, locator, prevLocator, args, options) {
213
213
  this._skyrampPage = skyrampPage
214
214
  this._locator = locator
215
215
  this._previousLocator = prevLocator
216
216
  this._args = args
217
217
  this._options = options
218
- this._hydration = hydration || false
219
218
  return new Proxy(this, {
220
219
  get(wrapper, prop, receiver) {
221
220
  // First, check if the property exists on the wrapper.
@@ -237,16 +236,8 @@ class SkyrampPlaywrightLocator {
237
236
  });
238
237
  }
239
238
 
240
- isHydration() {
241
- return this._hydration
242
- }
243
-
244
- isPrevHydration() {
245
- return this._previousLocator && this._previousLocator.isHydration()
246
- }
247
-
248
239
  async execute() {
249
- debug(`execute ${ this._locator}.${this.execFname} ${this.execParam ?? ''} ${this.execArgs ?? ''}`)
240
+ debug(` execute ${ this._locator}.${this.execFname} ${this.execParam ?? ''} ${this.execArgs ?? ''}`)
250
241
  const func = this._locator[this.execFname];
251
242
  return func.call(this._locator, this.execParam, this.execArgs);
252
243
  }
@@ -268,69 +259,119 @@ class SkyrampPlaywrightLocator {
268
259
  return ret;
269
260
  }
270
261
 
271
- // this is the function that does smart selector retry
262
+ wrapError(msg, error) {
263
+ let newMsg = msg;
264
+ if (this._skyrampPage.hasLLMChoices()) {
265
+ newMsg += this.generateLLMErrors();
266
+ }
267
+
268
+ error.message = error.message + "\n" + newMsg
269
+
270
+ return error
271
+ }
272
+
273
+ hydrationErrorMsg = "Potentially a hydration issue. Please add enough waitForTimeout()"
274
+
275
+ newPrevHyrdationErrorMsg() {
276
+ return `Cannot find locator ${this._locator} and likely a hydration issue on ${this._previousLocator._locator}.\n` +
277
+ `Please add enough waitForTimeout() on ${this._previoousLocator._locator}`;
278
+ }
279
+
280
+ newMultiLocatorErrorMsg() {
281
+ return `${this._locator} found ${this.locatorCount} locators. Please add "data-testid" attribute for a more stable locator`
282
+ }
283
+
284
+ async _retryWithLLM(error, msg1) {
285
+ // if API_KEY is not defined, throw an error here without trying
286
+ if (!process.env.API_KEY) {
287
+ error.message = msg1 + error.message;
288
+ throw error;
289
+ }
290
+
291
+ debug(` try to get suggessions from LLM for ${this._locator}`);
292
+ return retryWithLLM(this, error).then(result => {
293
+ this.LLMselector = true;
294
+ return result;
295
+ }).catch(newError => {
296
+ throw this.wrapError("", newError)
297
+ });
298
+ }
299
+
272
300
  async SmartRetryWithFallback(fname, param, ...args) {
273
301
  this.execFname = fname;
274
302
  this.execParam = param;
275
303
  this.execArgs = args;
276
304
 
277
305
  let locatorCount = await this._locator.count();
278
- debug(`handling ${ this._locator }.${ fname }, count = ${ locatorCount }`)
306
+ let currentUrl = this._skyrampPage._page.url();
307
+ debug(`handling ${ this._locator }.${ fname }, count = ${ locatorCount }, ${currentUrl}`);
279
308
  this.locatorCount = locatorCount
280
309
 
281
- let currentUrl = this._skyrampPage._page.url();
282
- debug(`current url = ${currentUrl}`);
310
+ //debug(`current url = ${currentUrl}`);
283
311
 
284
312
  // if locator exists, DOM is available
285
313
  if (locatorCount == 1) {
314
+ debug(` single locator for ${this._locator} identified, ${currentUrl}`);
286
315
  // we try action, this will most likely succeed
287
316
  // if it fails, there could be potentially a hydration issue we can retry after a little wait time
288
- return this.execute()
289
- .catch(error => {
317
+ try {
318
+ return await this.execute().then(result => {
319
+ return this._skyrampPage.checkNavigation(currentUrl, result);
320
+ });
321
+ } catch (error) {
322
+ debug(` first attempt of ${this._locator} failed, ${error.name}`);
290
323
  if (error.name == "TimeoutError") {
291
- debug(`locator ${this._locator} exists, but execution failed, wait a bit and retry`)
292
- this.wait(defaultWaitForTimeout)
324
+ // this is likely a hydration issue
325
+ // we wait a bit and retry
326
+ debug(` locator ${this._locator} exists, but execution failed, wait a bit and try again`);
327
+ await this.wait(defaultWaitForTimeout);
328
+
329
+ // Is this really necessary?
330
+ await this.execute().then(result => {
331
+ return this._skyrampPage.checkNavigation(currentUrl, result);
332
+ }).catch(() => {
333
+ debug(` failed second time and execute previous locator ${this._previousLocator._locator} again`);
334
+ this._previousLocator.execute();
335
+ }).catch(() => {
336
+ debug(` failed to execute previous locator ${this._previousLocator._locator} again, continue`);
337
+ });
293
338
 
294
339
  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);
340
+ debug(` third attempt on ${this._locator} failed ${newError.name}`);
341
+ if (newError.name == "TimeoutError") {
342
+ // this hadn't happened yet. we need to validate if this is indeed hydration case
343
+ return this._retryWithLLM(newError, this.hydrationErrorMsg)
344
+ }
345
+ if (newError.message.includes("strict mode violation")) {
346
+ return this._retryWithLLM(newError, this.newMultiLocatorErrorMsg());
302
347
  }
348
+ throw error;
303
349
  });
304
350
  }
351
+ if (error.message.includes("strict mode violation")) {
352
+ return this._retryWithLLM(error, this.newMultiLocatorErrorMsg());
353
+ }
354
+ // we do not handle the rest of the error cases, but simply forward error
305
355
  throw error;
306
- });
356
+ }
307
357
  } else if (locatorCount > 0) {
308
- // TODO
309
- debug(`multiple ${locatorCount} locators identified`)
358
+ debug(` multiple ${locatorCount} locators for ${this._locator} identified, ${currentUrl}`);
359
+ // wait a bit and retry before consulting with LLM
360
+ await this.wait(defaultWaitForTimeout);
361
+
362
+ locatorCount = await this._locator.count();
363
+ this.locatorCount = locatorCount
310
364
 
311
365
  if (locatorCount > 5) {
366
+ // we fail even without trying
312
367
  throw new Error(`${locatorCount} locators detected for ${this._locator}. Please add "data-testid" attribute for a more stable locator`);
313
368
  }
314
369
 
370
+ // this will likely fail, but we try to get error message generated by playwright
315
371
  return this.execute().then(result => {
316
- return result;
372
+ return this._skyrampPage.checkNavigation(currentUrl, result);
317
373
  }).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
- });
374
+ return this._retryWithLLM(error, this.newMultiLocatorErrorMsg());
334
375
  });
335
376
  } else {
336
377
  // if locator does not exist, we need to consider two cases
@@ -338,54 +379,44 @@ class SkyrampPlaywrightLocator {
338
379
  // second, if locator id is not correct
339
380
  // check if last step has potential hydration
340
381
  if (this._previousLocator && this._previousLocator.locatorCount == 0) {
341
- debug(`previous action ${this._previousLocator._locator} is potentially associated with hydration`);
382
+ debug(` previous action ${this._previousLocator._locator} is potentially associated with hydration, ${currentUrl}`);
342
383
  // wait for a short time to finish hydration
343
384
  await this.wait(defaultWaitForTimeout);
344
-
345
385
  const previousCount = await this._previousLocator.count();
346
- debug(` re-execute the previous one ${this._previousLocator._locator}, previous Locator count = ${previousCount}`);
347
-
386
+ debug(` re-execute the previous one ${this._previousLocator._locator}, new locator count = ${previousCount}, ${currentUrl}`);
348
387
  // re-execute the previous locator
349
388
  await this._previousLocator.execute().catch(() => {
350
389
  // log the failure but continues to the current one
351
- debug(`failed to execute previous locator ${this._previousLocator._locator} again`);
390
+ debug(` failed to execute previous locator ${this._previousLocator._locator} again, continue`);
352
391
  });
353
392
 
354
393
  try {
355
394
  // then execute the current one
356
- return await this.execute();
395
+ return await this.execute().then(result => {
396
+ return this._skyrampPage.checkNavigation(currentUrl, result);
397
+ });
357
398
  } catch (error) {
358
399
  if (error.name == "TimeoutError") {
359
- debug(`${this._locator} failed at first try. attempting again with some timeout`);
400
+ debug(` ${this._locator} failed at first try. attempting again with some timeout`);
360
401
  // wait for some time and re execute
361
402
  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
- });
403
+ return this.execute().then(result => {
404
+ return this._skyrampPage.checkNavigation(currentUrl, result);
405
+ }).catch(newError => {
406
+ return this._retryWithLLM(newError, this.newPrevHydrationErrorMsg());
378
407
  });
379
408
  }
409
+ if (error.message.includes("strict mode violation")) {
410
+ debug(` a rare case when multiple locators are detected on ${this._locator}`);
411
+ return this._retryWithLLM(error, this.newMultiLocatorErrorMsg());
412
+ }
380
413
  throw error;
381
414
  }
382
415
  } 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`);
416
+ if (this._previousLocator && this._previousLocator.LLMselector) {
417
+ debug(` ${this._locator} locator count is zero, but previous locator was selected by LLM`);
387
418
  } else {
388
- debug(`${this._locator} locator count is zero, but previous locator seems not related to hydration`);
419
+ debug(` ${this._locator} locator count is zero, but previous locator seems not related to hydration`);
389
420
  }
390
421
  // previous action may not be associated with hydration
391
422
  // then we just try current locator. could be a locator with a wrong id
@@ -393,36 +424,32 @@ class SkyrampPlaywrightLocator {
393
424
  await this.wait(defaultWaitForTimeout);
394
425
 
395
426
  this.locatorCount = await this._locator.count();
396
- debug(` after waiting locator count = ${this.locatorCount}, url = ${this._skyrampPage._page.url()}`);
427
+ debug(` after waiting locator ${this._locator} count = ${this.locatorCount}, ${currentUrl}`);
397
428
 
398
429
  try {
399
- return await this.execute();
430
+ return await this.execute().then(result => {
431
+ return this._skyrampPage.checkNavigation(currentUrl, result);
432
+ });
400
433
  } catch (error) {
401
434
  if (error.name == "TimeoutError") {
402
435
  debug(`${this._locator} failed at first try. attempting again with some timeout`);
403
436
  await this.wait(defaultWaitForTimeout);
404
-
405
- return this.execute().catch(newError => {
437
+ return this.execute().then(result=> {
438
+ return this._skyrampPage.checkNavigation(currentUrl, result);
439
+ }).catch(newError => {
406
440
  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;
441
+ return this._retryWithLLM(newError, this.hydrationErrorMsg);
442
+ }
443
+ if (newError.message.includes("strict mode violation")) {
444
+ return this._retryWithLLM(newError, this.newMultiLocatorErrorMsg());
423
445
  }
446
+ throw newError;
424
447
  });
425
448
  }
449
+ // if multiple locator found
450
+ if (this.locatorCount > 1 || error.message.includes("strict mode violation")) {
451
+ return this._retryWithLLM(error, this.newMultiLocatorErrorMsg());
452
+ }
426
453
  throw error;
427
454
  }
428
455
  }
@@ -613,7 +640,7 @@ class SkyrampPlaywrightLocator {
613
640
  }
614
641
 
615
642
  async wait(t) {
616
- debug(`wait for ${t}`);
643
+ debug(` wait for ${t}`);
617
644
  return this._skyrampPage._page.waitForTimeout(t);
618
645
  }
619
646
  }
@@ -621,7 +648,6 @@ class SkyrampPlaywrightLocator {
621
648
  class SkyrampPlaywrightPage {
622
649
  constructor(page) {
623
650
  this._page = page;
624
- this._curUrl = "";
625
651
  return new Proxy(this, {
626
652
  // The `get` trap is the key to forwarding.
627
653
  // This will foraward any methods not implemented in this struct
@@ -646,10 +672,6 @@ class SkyrampPlaywrightPage {
646
672
  });
647
673
  }
648
674
 
649
- getCurUrl() {
650
- return this._curUrl;
651
- }
652
-
653
675
  pushLocator(locator) {
654
676
  if (this.locators == undefined ) {
655
677
  this.locators = [];
@@ -670,7 +692,6 @@ class SkyrampPlaywrightPage {
670
692
 
671
693
  newSkyrampPlaywrightLocator(originalLocator, param, options) {
672
694
  let prevLocator = this.getLastLocator();
673
- const hydration = options && (options.hydration || false);
674
695
  /*
675
696
  if (prevLocator != null) {
676
697
  debug(`handling ${originalLocator} prev ${prevLocator._locator}`);
@@ -679,7 +700,7 @@ class SkyrampPlaywrightPage {
679
700
  }
680
701
  */
681
702
 
682
- let newLocator = new SkyrampPlaywrightLocator(this, originalLocator, prevLocator, [param], options, hydration);
703
+ let newLocator = new SkyrampPlaywrightLocator(this, originalLocator, prevLocator, [param], options);
683
704
  this.pushLocator(newLocator)
684
705
  return newLocator
685
706
  }
@@ -734,7 +755,6 @@ class SkyrampPlaywrightPage {
734
755
  } else {
735
756
  debug(`javascript not detected when visiting ${this._page.url()}`);
736
757
  }
737
- this._curUrl = this._page.url();
738
758
  return result;
739
759
  }
740
760
 
@@ -760,6 +780,15 @@ class SkyrampPlaywrightPage {
760
780
  }
761
781
  return true;
762
782
  }
783
+
784
+ async checkNavigation(original, result) {
785
+ const newURL = this._page.url()
786
+ if (newURL != original) {
787
+ debug(`page navigation to ${newURL} detected, wait a bit`);
788
+ await this._page.waitForTimeout(defaultWaitForTimeout);
789
+ }
790
+ return result;
791
+ }
763
792
  }
764
793
 
765
794
  function newSkyrampPlaywrightPage(page) {