@skyramp/skyramp 1.2.18 → 1.2.20

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.18",
3
+ "version": "1.2.20",
4
4
  "description": "module for leveraging skyramp cli functionality",
5
5
  "scripts": {
6
6
  "lint": "eslint 'src/**/*.js' 'src/**/*.ts' --fix",
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
+ export function newSkyrampPlaywrightPage(page: any): any;
@@ -0,0 +1,350 @@
1
+ /*
2
+ const lib = require('../lib');
3
+ const koffi = require('koffi');
4
+
5
+ const responseType = koffi.struct({
6
+ response: 'char*',
7
+ error: 'char*',
8
+ });
9
+
10
+ const improvePlaywrightSelectorWrapper = lib.func('improvePlywrightSelectorWrapper', responseType, ['string']);
11
+
12
+ async function improvePlaywrightSelectorWithLlm(originalSelector, errorMessage, domContext, pageTitle, pageURL) {
13
+ const requestData = {
14
+ "original_selector": originalSelector,
15
+ "error_message": errorMessage,
16
+ "dom_context": domContext,
17
+ "page_title": pageTitle,
18
+ "page_url": pageURL
19
+ };
20
+ const request = JSON.stringify(requestData);
21
+
22
+ return new Promise((resolve, reject) => {
23
+ improvePlaywrightSelectorWrapper.async(request, (err, res) => {
24
+ if (err) {
25
+ reject(err);
26
+ } else if (res) {
27
+ if (res.error != null) {
28
+ reject(res.error);
29
+ } else if (res.response != null) {
30
+ const response = JSON.parse(res.response);
31
+ resolve(response["suggestions"] ?? null);
32
+ } else {
33
+ reject(new Error('failed'));
34
+ }
35
+ } else {
36
+ reject(new Error('failed'));
37
+ }
38
+ });
39
+ });
40
+ }
41
+ */
42
+
43
+ function debug(...args) {
44
+ if (process.env.SKYRAMP_DEBUG == "true") {
45
+ console.log(...args);
46
+ }
47
+ }
48
+
49
+ class SkyrampPlaywrightLocator {
50
+ constructor(skyrampPage, locator, prevLocator, args, options, hydration) {
51
+ this._skyrampPage = skyrampPage
52
+ this._locator = locator
53
+ this._previousLocator = prevLocator
54
+ this._args = args
55
+ this._options = options
56
+ this._hydration = hydration || false
57
+ return new Proxy(this, {
58
+ get(wrapper, prop, receiver) {
59
+ // First, check if the property exists on the wrapper.
60
+ if (Reflect.has(wrapper, prop)) {
61
+ return Reflect.get(wrapper, prop, receiver);
62
+ }
63
+
64
+ // Otherwise, forward the call to the original object (`target`).
65
+ const value = Reflect.get(wrapper._locator, prop, wrapper._locator);
66
+
67
+ // If the property is a function, bind it to the original object.
68
+ // This ensures the correct `this` context is used when the method is called.
69
+ if (typeof value === 'function') {
70
+ return value.bind(wrapper._locator);
71
+ }
72
+
73
+ return value;
74
+ }
75
+ });
76
+ }
77
+
78
+ isHydration() {
79
+ return this._hydration
80
+ }
81
+
82
+ isPrevHydration() {
83
+ return this._previousLocator && this._previousLocator.isHydration()
84
+ }
85
+
86
+ shouldAttemptImprovement(errorMessage, errorType) {
87
+ const improvementKeywords = [
88
+ "timeout",
89
+ "not found",
90
+ "no element",
91
+ "not visible",
92
+ "not attached",
93
+ "selector resolved to hidden",
94
+ "element is not enabled",
95
+ ];
96
+
97
+ if (errorType != null) {
98
+ const playwrightErrorTypes = [
99
+ "TimeoutError",
100
+ "Error",
101
+ "LocatorAssertionError"
102
+ ];
103
+ if (playwrightErrorTypes.some(errType => errorType.name.endsWith(errType))) {
104
+ return true;
105
+ }
106
+ }
107
+
108
+ let errorLower = errorMessage.toLowerCase();
109
+ return improvementKeywords.some(keyword => errorLower.includes(keyword));
110
+ }
111
+
112
+ isSelectorMethod(methodName) {
113
+ // Check if a method uses selectors that can be improved.
114
+ const selectorMethods = [
115
+ 'click', 'fill', 'type', 'press', 'check', 'uncheck', 'select_option',
116
+ 'hover', 'focus', 'blur', 'scroll_into_view_if_needed', 'screenshot',
117
+ 'text_content', 'inner_text', 'inner_html', 'get_attribute', 'is_visible',
118
+ 'is_enabled', 'is_checked', 'is_disabled', 'is_editable', 'is_hidden'
119
+ ]
120
+ return selectorMethods.includes(methodName)
121
+ }
122
+
123
+ async execute() {
124
+ debug(`execute ${ this._locator} with ${this.execParam} ${this.execArgs}`)
125
+ const func = this._locator[this.execFname];
126
+ return func.call(this._locator, this.execParam, this.execArgs);
127
+ }
128
+
129
+ // this is the function that does smart selector retry
130
+ async SmartRetryWithFallback(fname, param, ...args) {
131
+ this.execFname = fname;
132
+ this.execParam = param;
133
+ this.execArgs = args;
134
+
135
+ let locatorCount = await this._locator.count();
136
+ debug(`handling ${ fname } of ${ this._locator }, count = ${ locatorCount }`)
137
+ this.locatorCount = locatorCount
138
+
139
+ // if locator exists, DOM is available
140
+ if (locatorCount == 1) {
141
+ // we try action, this will most likely succeed
142
+ // if it fails, there could be potentially a hydration issue we can retry after a little wait time
143
+ return this.execute()
144
+ .catch(error => {
145
+ if (error.name == "TimeoutError") {
146
+ return this.execute()
147
+ .catch(error => {
148
+ throw new Error("Potential hydration. Please add enough waitForTimeout() or use hydration flag", error);
149
+ });
150
+ }
151
+ throw error;
152
+ });
153
+ } else if (locatorCount > 0) {
154
+ // TODO
155
+ debug("multiple locators identified")
156
+ return this.execute();
157
+ } else {
158
+ // if locator does not exist, we need to consider two cases
159
+ // one is if any actions that required hydration did not work
160
+ // second, if locator id is not correct
161
+ try {
162
+ // check if last step has potential hydration
163
+ if (this._previousLocator && (this.isPrevHydration() || this._previousLocator.locatorCount == 0)) {
164
+ debug(`previous action ${this._previousLocator} is potentially associated with hydration`);
165
+ // wait for a short time to finish hydration
166
+ await this._skyrampPage._page.waitForTimeout(1500);
167
+ // then re-execute the previous locator
168
+ await this._previousLocator.execute();
169
+ // then execute the current one
170
+ return this.execute();
171
+ } else {
172
+ // previous action may not be associated with hydration
173
+ // but we still try
174
+ return this.execute();
175
+ }
176
+ } catch (error) {
177
+ if (error.name == "TimeoutError") {
178
+ return this.execute()
179
+ .catch(error => {
180
+ throw new Error("Potential hydration. Please add enough waitForTimeout() or use hydration flag", error);
181
+ });
182
+ } else {
183
+ // TODO
184
+ throw error;
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ async click(...args) {
191
+ return this.SmartRetryWithFallback("click", null, ...args);
192
+ }
193
+
194
+ async fill(text, ...args) {
195
+ return this.SmartRetryWithFallback("fill", text, ...args);
196
+ }
197
+
198
+ async type(text, ...args) {
199
+ return this.SmartRetryWithFallback("type", text, ...args);
200
+ }
201
+
202
+ async press(key, ...args) {
203
+ return this.SmartRetryWithFallback("press", key, ...args);
204
+ }
205
+
206
+ async check(...args) {
207
+ return this.SmartRetryWithFallback("check", null, ...args);
208
+ }
209
+
210
+ async uncheck(...args) {
211
+ return this.SmartRetryWithFallback("check", null, ...args);
212
+ }
213
+
214
+ async selectOption(value, ...args) {
215
+ return this.SmartRetryWithFallback("selectOption", value, ...args);
216
+ }
217
+
218
+ async hover(...args) {
219
+ return this.SmartRetryWithFallback("hover", null, ...args);
220
+ }
221
+
222
+ async textContent(...args) {
223
+ return this.SmartRetryWithFallback("textContent", null, ...args);
224
+ }
225
+
226
+ async isVisible(...args) {
227
+ return this.SmartRetryWithFallback("isVisible", null, ...args);
228
+ }
229
+
230
+ nth(index) {
231
+ // get nth element - return a new SkyrampPlaywrightLocator
232
+ let new_locator = this._locator.nth(index)
233
+ return this._skyrampPage.newSkyrampPlaywrightLocator(new_locator, index, null);
234
+ }
235
+
236
+ first() {
237
+ // get first element - return a new SkyrampPlaywrightLocator
238
+ let new_locator = this._locator.first()
239
+ return this._skyrampPage.newSkyrampPlaywrightLocator(new_locator, null, null);
240
+ }
241
+
242
+ last() {
243
+ // get last element - return a new SkyrampPlaywrightLocator
244
+ let new_locator = this._locator.last()
245
+ return this._skyrampPage.newSkyrampPlaywrightLocator(new_locator, null, null);
246
+ }
247
+ }
248
+
249
+ class SkyrampPlaywrightPage {
250
+ constructor(page) {
251
+ this._page = page;
252
+ return new Proxy(this, {
253
+ // The `get` trap is the key to forwarding.
254
+ // This will foraward any methods not implemented in this struct
255
+ // to be handled by the original class (i.e., playwright page object)
256
+ get(wrapper, prop, receiver) {
257
+ // First, check if the property exists on the wrapper.
258
+ if (Reflect.has(wrapper, prop)) {
259
+ return Reflect.get(wrapper, prop, receiver);
260
+ }
261
+
262
+ // Otherwise, forward the call to the original object (`target`).
263
+ const value = Reflect.get(wrapper._page, prop, wrapper._page);
264
+
265
+ // If the property is a function, bind it to the original object.
266
+ // This ensures the correct `this` context is used when the method is called.
267
+ if (typeof value === 'function') {
268
+ return value.bind(wrapper._page);
269
+ }
270
+
271
+ return value;
272
+ }
273
+ });
274
+ }
275
+
276
+ pushLocator(locator) {
277
+ if (this.locators == undefined ) {
278
+ this.locators = [];
279
+ }
280
+ this.locators.push(locator);
281
+ }
282
+
283
+ getLastLocator() {
284
+ if (this.locators == undefined) {
285
+ return null
286
+ }
287
+
288
+ if (this.locators.length == 0) {
289
+ return null
290
+ }
291
+ return this.locators.at(-1)
292
+ }
293
+
294
+ newSkyrampPlaywrightLocator(originalLocator, param, options) {
295
+ let prevLocator = this.getLastLocator()
296
+ const hydration = options && (options.hydration || false)
297
+ debug("intercepting", originalLocator)
298
+ let newLocator = new SkyrampPlaywrightLocator(this, originalLocator, prevLocator, [param], options, hydration);
299
+ this.pushLocator(newLocator)
300
+ return newLocator
301
+ }
302
+
303
+ locator(selector, options) {
304
+ const originalLocator = this._page.locator(selector, options);
305
+ return this.newSkyrampPlaywrightLocator(originalLocator, selector, options)
306
+ }
307
+
308
+ getByRole(role, options) {
309
+ const originalLocator = this._page.getByRole(role, options);
310
+ return this.newSkyrampPlaywrightLocator(originalLocator, role, options)
311
+ }
312
+
313
+ getByText(text, options) {
314
+ const originalLocator = this._page.getByText(text, options);
315
+ return this.newSkyrampPlaywrightLocator(originalLocator, text, options)
316
+ }
317
+
318
+ getByLabel(label, options) {
319
+ const originalLocator = this._page.getByLabel(label, options);
320
+ return this.newSkyrampPlaywrightLocator(originalLocator, label, options)
321
+ }
322
+
323
+ getByTestId(testId, options) {
324
+ const originalLocator = this._page.getByTestId(testId);
325
+ return this.newSkyrampPlaywrightLocator(originalLocator, testId, options)
326
+ }
327
+
328
+ getByTitle(title, options) {
329
+ const originalLocator = this._page.getByTitle(title, options);
330
+ return this.newSkyrampPlaywrightLocator(originalLocator, title, options)
331
+ }
332
+
333
+ getByPlaceholder(placeholder, options) {
334
+ const originalLocator = this._page.getByPlaceholder(placeholder, options);
335
+ return this.newSkyrampPlaywrightLocator(originalLocator, placeholder, options)
336
+ }
337
+
338
+ getByAltText(alt, options) {
339
+ const originalLocator = this._page.getByAltText(alt, options);
340
+ return this.newSkyrampPlaywrightLocator(originalLocator, alt, options)
341
+ }
342
+ }
343
+
344
+ function newSkyrampPlaywrightPage(page) {
345
+ return new SkyrampPlaywrightPage(page);
346
+ }
347
+
348
+ module.exports = {
349
+ newSkyrampPlaywrightPage,
350
+ };
package/src/index.d.ts CHANGED
@@ -18,3 +18,4 @@ export * from './classes/LoadTestConfig';
18
18
  export * from './classes/AsyncTestStatus';
19
19
  export * from './utils';
20
20
  export * from './function';
21
+ export * from './classes/SmartPlaywright';
package/src/index.js CHANGED
@@ -19,6 +19,7 @@ const AsyncTestStatus = require('./classes/AsyncTestStatus');
19
19
  const MockV2 = require('./classes/MockV2');
20
20
  const { getValue, checkSchema, iterate } = require('./utils');
21
21
  const { checkStatusCode } = require('./function');
22
+ const { newSkyrampPlaywrightPage } = require('./classes/SmartPlaywright');
22
23
 
23
24
  module.exports = {
24
25
  SkyrampClient,
@@ -45,4 +46,5 @@ module.exports = {
45
46
  checkStatusCode,
46
47
  checkSchema,
47
48
  iterate,
49
+ newSkyrampPlaywrightPage,
48
50
  }