@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 +1 -1
- package/src/classes/SmartPlaywright.d.ts +2 -0
- package/src/classes/SmartPlaywright.js +350 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +2 -0
package/package.json
CHANGED
|
@@ -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
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
|
}
|