@skyramp/skyramp 1.2.18 → 1.2.19

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.19",
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,326 @@
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
+ } else {
157
+ // if locator does not exist, we need to consider two cases
158
+ // one is if any actions that required hydration did not work
159
+ // second, if locator id is not correct
160
+ try {
161
+ // check if last step has potential hydration
162
+ if (this._previousLocator && (this.isPrevHydration() || this._previousLocator.locatorCount == 0)) {
163
+ debug(`previous action ${this._previousLocator} is potentially associated with hydration`);
164
+ // wait for a short time to finish hydration
165
+ await this._skyrampPage._page.waitForTimeout(1500);
166
+ // then re-execute the previous locator
167
+ await this._previousLocator.execute();
168
+ // then execute the current one
169
+ return await this.execute();
170
+ } else {
171
+ // previous action may not be associated with hydration
172
+ // but we still try
173
+ return await this.execute();
174
+ }
175
+ } catch (error) {
176
+ if (error.name == "TimeoutError") {
177
+ return this.execute()
178
+ .catch(error => {
179
+ throw new Error("Potential hydration. Please add enough waitForTimeout() or use hydration flag", error);
180
+ });
181
+ } else {
182
+ // TODO
183
+ throw error;
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ async click(...args) {
190
+ return this.SmartRetryWithFallback("click", null, ...args)
191
+ }
192
+
193
+ async fill(text, ...args) {
194
+ return this.SmartRetryWithFallback("fill", text, ...args)
195
+ }
196
+
197
+ async type(text, ...args) {
198
+ return this.SmartRetryWithFallback("type", text, ...args)
199
+ }
200
+
201
+ async press(key, ...args) {
202
+ return this.SmartRetryWithFallback("press", key, ...args)
203
+ }
204
+
205
+ async check(...args) {
206
+ return this.SmartRetryWithFallback("check", null, ...args)
207
+ }
208
+
209
+ async uncheck(...args) {
210
+ return this.SmartRetryWithFallback("check", null, ...args)
211
+ }
212
+
213
+ async selectOption(value, ...args) {
214
+ return this.SmartRetryWithFallback("selectOption", value, ...args)
215
+ }
216
+
217
+ async hover(...args) {
218
+ return this.SmartRetryWithFallback("hover", null, ...args)
219
+ }
220
+
221
+ async textContent(...args) {
222
+ return this.SmartRetryWithFallback("textContent", null, ...args)
223
+ }
224
+
225
+ async isVisible(...args) {
226
+ return this.SmartRetryWithFallback("isVisible", null, ...args)
227
+ }
228
+ }
229
+
230
+ class SkyrampPlaywrightPage {
231
+ constructor(page) {
232
+ this._page = page;
233
+ return new Proxy(this, {
234
+ // The `get` trap is the key to forwarding.
235
+ // This will foraward any methods not implemented in this struct
236
+ // to be handled by the original class (i.e., playwright page object)
237
+ get(wrapper, prop, receiver) {
238
+ // First, check if the property exists on the wrapper.
239
+ if (Reflect.has(wrapper, prop)) {
240
+ return Reflect.get(wrapper, prop, receiver);
241
+ }
242
+
243
+ // Otherwise, forward the call to the original object (`target`).
244
+ const value = Reflect.get(wrapper._page, prop, wrapper._page);
245
+
246
+ // If the property is a function, bind it to the original object.
247
+ // This ensures the correct `this` context is used when the method is called.
248
+ if (typeof value === 'function') {
249
+ return value.bind(wrapper._page);
250
+ }
251
+
252
+ return value;
253
+ }
254
+ });
255
+ }
256
+
257
+ pushLocator(locator) {
258
+ if (this.locators == undefined ) {
259
+ this.locators = [];
260
+ }
261
+ this.locators.push(locator);
262
+ }
263
+
264
+ getLastLocator() {
265
+ if (this.locators == undefined) {
266
+ return null
267
+ }
268
+
269
+ if (this.locators.length == 0) {
270
+ return null
271
+ }
272
+ return this.locators.at(-1)
273
+ }
274
+
275
+ newSkyrampPlaywrightLocator(originalLocator, param, options) {
276
+ let prevLocator = this.getLastLocator()
277
+ const hydration = options && (options.hydration || false)
278
+ debug("intercepting", originalLocator)
279
+ let newLocator = new SkyrampPlaywrightLocator(this, originalLocator, prevLocator, [param], options, hydration);
280
+ this.pushLocator(newLocator)
281
+ return newLocator
282
+ }
283
+
284
+ getByRole(role, options) {
285
+ const originalLocator = this._page.getByRole(role, options);
286
+ return this.newSkyrampPlaywrightLocator(originalLocator, role, options)
287
+ }
288
+
289
+ getByText(text, options) {
290
+ const originalLocator = this._page.getByText(text, options);
291
+ return this.newSkyrampPlaywrightLocator(originalLocator, text, options)
292
+ }
293
+
294
+ getByLabel(label, options) {
295
+ const originalLocator = this._page.getByLabel(label, options);
296
+ return this.newSkyrampPlaywrightLocator(originalLocator, label, options)
297
+ }
298
+
299
+ getByTestId(testId, options) {
300
+ const originalLocator = this._page.getByTestId(testId);
301
+ return this.newSkyrampPlaywrightLocator(originalLocator, testId, options)
302
+ }
303
+
304
+ getByTitle(title, options) {
305
+ const originalLocator = this._page.getByTitle(title, options);
306
+ return this.newSkyrampPlaywrightLocator(originalLocator, title, options)
307
+ }
308
+
309
+ getByPlaceholder(placeholder, options) {
310
+ const originalLocator = this._page.getByPlaceholder(placeholder, options);
311
+ return this.newSkyrampPlaywrightLocator(originalLocator, placeholder, options)
312
+ }
313
+
314
+ getByAltText(alt, options) {
315
+ const originalLocator = this._page.getByAltText(alt, options);
316
+ return this.newSkyrampPlaywrightLocator(originalLocator, alt, options)
317
+ }
318
+ }
319
+
320
+ function newSkyrampPlaywrightPage(page) {
321
+ return new SkyrampPlaywrightPage(page);
322
+ }
323
+
324
+ module.exports = {
325
+ newSkyrampPlaywrightPage,
326
+ };
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
  }