@skyramp/skyramp 1.2.22 → 1.2.24

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.22",
3
+ "version": "1.2.24",
4
4
  "description": "module for leveraging skyramp cli functionality",
5
5
  "scripts": {
6
6
  "lint": "eslint 'src/**/*.js' 'src/**/*.ts' --fix",
@@ -267,4 +267,12 @@ 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
+ timestamp: Date
277
+ ): string;
270
278
  }
@@ -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,41 @@ class SkyrampClient {
966
968
  );
967
969
  });
968
970
  }
971
+
972
+ async fetchLatestOtp(email, timestamp) {
973
+ var latestGetResponse
974
+ // if timestamp is not given,
975
+ // we use the time that this client was instantiated
976
+ if (timestamp == undefined || timestamp == null) {
977
+ timestamp = this.timestamp;
978
+ }
979
+
980
+ if (!email.includes("@otp.skyramp.dev")) {
981
+ throw new Error("non-Skyramp email is not supported")
982
+ }
983
+
984
+ const username = email.replace("@otp.skyramp.dev", "")
985
+
986
+ for (let i = 0; i < 10; i ++) {
987
+ // Execute Request
988
+ latestGetResponse = await this.sendRequest({
989
+ url: "https://tokenize.skyramp.dev",
990
+ path: `/msg/${username}/latest`,
991
+ method: "GET"
992
+ });
993
+
994
+ if (latestGetResponse.statusCode != 200) {
995
+ await new Promise(resolve => setTimeout(resolve, 1000));
996
+ continue
997
+ }
998
+
999
+ let codeTime = new Date(utils.getValue(latestGetResponse.responseBody, "timestamp"));
1000
+ if (codeTime > timestamp) {
1001
+ return utils.getValue(latestGetResponse.responseBody, "otp")
1002
+ }
1003
+ await new Promise(resolve => setTimeout(resolve, 1000));
1004
+ }
1005
+ }
969
1006
  }
970
1007
 
971
1008
  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();
@@ -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} replacing ${newLocator}`, error.name);
201
199
  });
202
200
  }
203
201
  } catch {
@@ -205,7 +203,9 @@ 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 {
@@ -246,7 +246,7 @@ class SkyrampPlaywrightLocator {
246
246
  }
247
247
 
248
248
  async execute() {
249
- debug(`execute ${ this._locator}.${this.execFname} ${this.execParam ?? ''} ${this.execArgs ?? ''}`)
249
+ debug(` execute ${ this._locator}.${this.execFname} ${this.execParam ?? ''} ${this.execArgs ?? ''}`)
250
250
  const func = this._locator[this.execFname];
251
251
  return func.call(this._locator, this.execParam, this.execArgs);
252
252
  }
@@ -268,69 +268,111 @@ class SkyrampPlaywrightLocator {
268
268
  return ret;
269
269
  }
270
270
 
271
- // this is the function that does smart selector retry
271
+ wrapError(msg, error) {
272
+ let newMsg = msg;
273
+ if (this._skyrampPage.hasLLMChoices()) {
274
+ newMsg += this.generateLLMErrors();
275
+ }
276
+
277
+ error.message = error.message + "\n" + newMsg
278
+
279
+ return error
280
+ }
281
+
282
+ hydrationErrorMsg = "Potentially a hydration issue. Please add enough waitForTimeout()"
283
+
284
+ newPrevHyrdationErrorMsg() {
285
+ return `Cannot find locator ${this._locator} and likely a hydration issue on ${this._previousLocator._locator}.\n` +
286
+ `Please add enough waitForTimeout() on ${this._previoousLocator._locator}`;
287
+ }
288
+
289
+ newMultiLocatorErrorMsg() {
290
+ return `${this._locator} found ${this.locatorCount} locators. Please add "data-testid" attribute for a more stable locator`
291
+ }
292
+
293
+ async _retryWithLLM(error, msg1) {
294
+ // if API_KEY is not defined, throw an error here without trying
295
+ if (!process.env.API_KEY) {
296
+ error.message = msg1 + error.message;
297
+ throw error;
298
+ }
299
+
300
+ debug(` try to get suggessions from LLM for ${this._locator}`);
301
+ return retryWithLLM(this, error).then(result => {
302
+ this.LLMselector = true;
303
+ return result;
304
+ }).catch(newError => {
305
+ throw this.wrapError("", newError)
306
+ });
307
+ }
308
+
272
309
  async SmartRetryWithFallback(fname, param, ...args) {
273
310
  this.execFname = fname;
274
311
  this.execParam = param;
275
312
  this.execArgs = args;
276
313
 
277
314
  let locatorCount = await this._locator.count();
278
- debug(`handling ${ this._locator }.${ fname }, count = ${ locatorCount }`)
315
+ let currentUrl = this._skyrampPage._page.url();
316
+ debug(`handling ${ this._locator }.${ fname }, count = ${ locatorCount }, ${currentUrl}`);
279
317
  this.locatorCount = locatorCount
280
318
 
281
- let currentUrl = this._skyrampPage._page.url();
282
- debug(`current url = ${currentUrl}`);
319
+ //debug(`current url = ${currentUrl}`);
283
320
 
284
321
  // if locator exists, DOM is available
285
322
  if (locatorCount == 1) {
323
+ debug(` single locator for ${this._locator} identified, ${currentUrl}`);
286
324
  // we try action, this will most likely succeed
287
325
  // 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 => {
326
+ try {
327
+ return await this.execute().then(result => {
328
+ return this._skyrampPage.checkNavigation(currentUrl, result);
329
+ });
330
+ } catch (error) {
331
+ debug(` first attempt of ${this._locator} failed, ${error.name}`);
290
332
  if (error.name == "TimeoutError") {
291
- debug(`locator ${this._locator} exists, but execution failed, wait a bit and retry`)
292
- this.wait(defaultWaitForTimeout)
333
+ // this is likely a hydration issue
334
+ // we wait a bit and retry
335
+ debug(` locator ${this._locator} exists, but execution failed, wait a bit and try from last step retry`);
336
+ await this.wait(defaultWaitForTimeout);
337
+
338
+ // Is this really necessary?
339
+ await this.execute().catch(() => {
340
+ debug(` failed second time and execute previous locator ${this._previousLocator._locator} again`);
341
+ return this._previousLocator.execute();
342
+ }).catch(() => {
343
+ debug(` failed to execute previous locator ${this._previousLocator._locator} again, continue`);
344
+ });
293
345
 
294
346
  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);
347
+ debug(` second attempt on ${this._locator} failed ${newError.name}`);
348
+ if (newError.name == "TimeoutError") {
349
+ // this hadn't happened yet. we need to validate if this is indeed hydration case
350
+ throw this.wrapError(this.hydrationErrorMsg, newError);
302
351
  }
352
+ if (newError.message.includes("strict mode violation")) {
353
+ return this._retryWithLLM(newError, this.newMultiLocatorErrorMsg());
354
+ }
355
+ throw error;
303
356
  });
304
357
  }
358
+ if (error.message.includes("strict mode violation")) {
359
+ return this._retryWithLLM(error, this.newMultiLocatorErrorMsg());
360
+ }
361
+ // we do not handle the rest of the error cases, but simply forward error
305
362
  throw error;
306
- });
363
+ }
307
364
  } else if (locatorCount > 0) {
308
- // TODO
309
- debug(`multiple ${locatorCount} locators identified`)
310
-
365
+ debug(` multiple ${locatorCount} locators for ${this._locator} identified, ${currentUrl}`);
311
366
  if (locatorCount > 5) {
367
+ // we fail even without trying
312
368
  throw new Error(`${locatorCount} locators detected for ${this._locator}. Please add "data-testid" attribute for a more stable locator`);
313
369
  }
314
370
 
371
+ // this will likely fail, but we try to get error message generated by playwright
315
372
  return this.execute().then(result => {
316
- return result;
373
+ return this._skyrampPage.checkNavigation(currentUrl, result);
317
374
  }).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
- });
375
+ return this._retryWithLLM(error, this.newMultiLocatorErrorMsg());
334
376
  });
335
377
  } else {
336
378
  // if locator does not exist, we need to consider two cases
@@ -338,54 +380,42 @@ class SkyrampPlaywrightLocator {
338
380
  // second, if locator id is not correct
339
381
  // check if last step has potential hydration
340
382
  if (this._previousLocator && this._previousLocator.locatorCount == 0) {
341
- debug(`previous action ${this._previousLocator._locator} is potentially associated with hydration`);
383
+ debug(` previous action ${this._previousLocator._locator} is potentially associated with hydration, ${currentUrl}`);
342
384
  // wait for a short time to finish hydration
343
385
  await this.wait(defaultWaitForTimeout);
344
-
345
386
  const previousCount = await this._previousLocator.count();
346
- debug(` re-execute the previous one ${this._previousLocator._locator}, previous Locator count = ${previousCount}`);
347
-
387
+ debug(` re-execute the previous one ${this._previousLocator._locator}, new locator count = ${previousCount}, ${this._skyrampPage._page.url()}`);
348
388
  // re-execute the previous locator
349
389
  await this._previousLocator.execute().catch(() => {
350
390
  // log the failure but continues to the current one
351
- debug(`failed to execute previous locator ${this._previousLocator._locator} again`);
391
+ debug(` failed to execute previous locator ${this._previousLocator._locator} again, continue`);
352
392
  });
353
393
 
354
394
  try {
355
395
  // then execute the current one
356
- return await this.execute();
396
+ return await this.execute().then(result => {
397
+ return this._skyrampPage.checkNavigation(currentUrl, result);
398
+ });
357
399
  } catch (error) {
358
400
  if (error.name == "TimeoutError") {
359
- debug(`${this._locator} failed at first try. attempting again with some timeout`);
401
+ debug(` ${this._locator} failed at first try. attempting again with some timeout`);
360
402
  // wait for some time and re execute
361
403
  await this.wait(defaultWaitForTimeout);
362
-
363
404
  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
- });
405
+ return this._retryWithLLM(newError, this.newPrevHydrationErrorMsg());
378
406
  });
379
407
  }
408
+ if (error.message.includes("strict mode violation")) {
409
+ debug(` a rare case when multiple locators are detected on ${this._locator}`);
410
+ return this._retryWithLLM(error, this.newMultiLocatorErrorMsg());
411
+ }
380
412
  throw error;
381
413
  }
382
414
  } 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`);
415
+ if (this._previousLocator && this._previousLocator.LLMselector) {
416
+ debug(` ${this._locator} locator count is zero, but previous locator was selected by LLM`);
387
417
  } else {
388
- debug(`${this._locator} locator count is zero, but previous locator seems not related to hydration`);
418
+ debug(` ${this._locator} locator count is zero, but previous locator seems not related to hydration`);
389
419
  }
390
420
  // previous action may not be associated with hydration
391
421
  // then we just try current locator. could be a locator with a wrong id
@@ -393,36 +423,30 @@ class SkyrampPlaywrightLocator {
393
423
  await this.wait(defaultWaitForTimeout);
394
424
 
395
425
  this.locatorCount = await this._locator.count();
396
- debug(` after waiting locator count = ${this.locatorCount}, url = ${this._skyrampPage._page.url()}`);
426
+ debug(` after waiting locator ${this._locator} count = ${this.locatorCount}, ${this._skyrampPage._page.url()}`);
397
427
 
398
428
  try {
399
- return await this.execute();
429
+ return await this.execute().then(result => {
430
+ return this._skyrampPage.checkNavigation(currentUrl, result);
431
+ });
400
432
  } catch (error) {
401
433
  if (error.name == "TimeoutError") {
402
434
  debug(`${this._locator} failed at first try. attempting again with some timeout`);
403
435
  await this.wait(defaultWaitForTimeout);
404
-
405
436
  return this.execute().catch(newError => {
406
437
  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;
438
+ return this._retryWithLLM(newError, this.hydrationErrorMsg);
439
+ }
440
+ if (newError.message.includes("strict mode violation")) {
441
+ return this._retryWithLLM(newError, this.newMultiLocatorErrorMsg());
423
442
  }
443
+ throw newError;
424
444
  });
425
445
  }
446
+ // if multiple locator found
447
+ if (this.locatorCount > 1 || error.message.includes("strict mode violation")) {
448
+ return this._retryWithLLM(error, this.newMultiLocatorErrorMsg());
449
+ }
426
450
  throw error;
427
451
  }
428
452
  }
@@ -613,7 +637,7 @@ class SkyrampPlaywrightLocator {
613
637
  }
614
638
 
615
639
  async wait(t) {
616
- debug(`wait for ${t}`);
640
+ debug(` wait for ${t}`);
617
641
  return this._skyrampPage._page.waitForTimeout(t);
618
642
  }
619
643
  }
@@ -760,6 +784,15 @@ class SkyrampPlaywrightPage {
760
784
  }
761
785
  return true;
762
786
  }
787
+
788
+ async checkNavigation(original, result) {
789
+ const newURL = this._page.url()
790
+ if (newURL != original) {
791
+ debug(`page navigation to ${newURL} detected, wait a bit`);
792
+ await this._page.waitForTimeout(1500);
793
+ }
794
+ return result;
795
+ }
763
796
  }
764
797
 
765
798
  function newSkyrampPlaywrightPage(page) {