@skyramp/skyramp 1.2.20 → 1.2.22
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 +3 -2
- package/src/classes/SkyrampClient.d.ts +110 -37
- package/src/classes/SmartPlaywright.d.ts +3 -0
- package/src/classes/SmartPlaywright.js +572 -143
- package/src/function.d.ts +20 -0
- package/src/index.d.ts +21 -21
- package/src/index.js +2 -1
- package/src/utils.d.ts +68 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyramp/skyramp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.22",
|
|
4
4
|
"description": "module for leveraging skyramp cli functionality",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"lint": "eslint 'src/**/*.js' 'src/**/*.ts' --fix",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"src/classes/*.ts"
|
|
18
18
|
],
|
|
19
19
|
"main": "src/index.js",
|
|
20
|
+
"types": "src/index.d.ts",
|
|
20
21
|
"author": "",
|
|
21
22
|
"license": "MIT",
|
|
22
23
|
"dependencies": {
|
|
@@ -30,4 +31,4 @@
|
|
|
30
31
|
"@typescript-eslint/parser": "^6.14.0",
|
|
31
32
|
"eslint": "^8.55.0"
|
|
32
33
|
}
|
|
33
|
-
}
|
|
34
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { ResponseValue } from
|
|
2
|
-
import { Endpoint } from
|
|
3
|
-
import { Scenario } from
|
|
4
|
-
import {TrafficConfig} from
|
|
5
|
-
import { Protocol } from
|
|
6
|
-
import { AsyncScenario } from
|
|
7
|
-
import { LoadTestConfig } from
|
|
8
|
-
import
|
|
9
|
-
import { MultipartParam } from
|
|
10
|
-
import { MockV2 } from
|
|
1
|
+
import { ResponseValue } from "..";
|
|
2
|
+
import { Endpoint } from "./Endpoint";
|
|
3
|
+
import { Scenario } from "./Scenario";
|
|
4
|
+
import { TrafficConfig } from "./TrafficConfig";
|
|
5
|
+
import { Protocol } from "./Protocol";
|
|
6
|
+
import { AsyncScenario } from "./AsyncScenario";
|
|
7
|
+
import { LoadTestConfig } from "./LoadTestConfig";
|
|
8
|
+
import AsyncTestStatus = require("./AsyncTestStatus");
|
|
9
|
+
import { MultipartParam } from "./MultipartParam";
|
|
10
|
+
import { MockV2 } from "./MockV2";
|
|
11
11
|
|
|
12
12
|
export enum Language {
|
|
13
|
-
PYTHON =
|
|
13
|
+
PYTHON = "python",
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
interface testerStartV1Options {
|
|
@@ -21,8 +21,8 @@ interface testerStartV1Options {
|
|
|
21
21
|
address?: string;
|
|
22
22
|
scenario: Scenario | [Scenario];
|
|
23
23
|
testName: string;
|
|
24
|
-
globalHeaders?: {[headerName: string]: string};
|
|
25
|
-
globalVars?: {[variableName: string]: string};
|
|
24
|
+
globalHeaders?: { [headerName: string]: string };
|
|
25
|
+
globalVars?: { [variableName: string]: string };
|
|
26
26
|
generateTestReport?: boolean;
|
|
27
27
|
isDockerenv?: boolean;
|
|
28
28
|
}
|
|
@@ -40,12 +40,20 @@ interface SendRequestV2Options {
|
|
|
40
40
|
path: string;
|
|
41
41
|
method: string;
|
|
42
42
|
body?: string;
|
|
43
|
-
headers?: {[headerName: string]: string};
|
|
44
|
-
cookies?: {[cookieName: string]: string};
|
|
45
|
-
dataOverride?: {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
headers?: { [headerName: string]: string };
|
|
44
|
+
cookies?: { [cookieName: string]: string };
|
|
45
|
+
dataOverride?: {
|
|
46
|
+
[dataName: string]: string | number | boolean | object | null;
|
|
47
|
+
};
|
|
48
|
+
pathParams?: {
|
|
49
|
+
[pathName: string]: string | number | boolean | object | null;
|
|
50
|
+
};
|
|
51
|
+
queryParams?: {
|
|
52
|
+
[queryName: string]: string | number | boolean | object | null;
|
|
53
|
+
};
|
|
54
|
+
formParams?: {
|
|
55
|
+
[formParamName: string]: string | number | boolean | object | null;
|
|
56
|
+
};
|
|
49
57
|
multipartParams?: Array<MultipartParam>;
|
|
50
58
|
expectedCode?: string;
|
|
51
59
|
description?: string;
|
|
@@ -136,36 +144,97 @@ interface SendScenarioOptions {
|
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
export declare class SkyrampClient {
|
|
139
|
-
constructor(
|
|
147
|
+
constructor(
|
|
148
|
+
kubeconfigPath?: string,
|
|
149
|
+
clusterName?: string,
|
|
150
|
+
context?: string,
|
|
151
|
+
userToken?: string
|
|
152
|
+
);
|
|
140
153
|
constructor(options: SkyrampClientOptions);
|
|
141
154
|
login(): Promise<string>;
|
|
142
155
|
logout(): Promise<string>;
|
|
143
156
|
applyLocal(): Promise<void>;
|
|
144
|
-
addKubeconfig(
|
|
157
|
+
addKubeconfig(
|
|
158
|
+
context: string,
|
|
159
|
+
clusterName: string,
|
|
160
|
+
kubeconfigPath: string
|
|
161
|
+
): Promise<void>;
|
|
145
162
|
removeLocal(): Promise<void>;
|
|
146
163
|
removeCluster(clusterName?: string): Promise<void>;
|
|
147
|
-
mockerApply(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
164
|
+
mockerApply(
|
|
165
|
+
namespace: string,
|
|
166
|
+
kubePath: string,
|
|
167
|
+
kubeContext: string,
|
|
168
|
+
clusterName: string,
|
|
169
|
+
address: string,
|
|
170
|
+
endpoint: Endpoint
|
|
171
|
+
): Promise<void>;
|
|
172
|
+
deploySkyrampWorker(
|
|
173
|
+
namespace?: string,
|
|
174
|
+
workerImage?: string,
|
|
175
|
+
localImage?: boolean,
|
|
176
|
+
kubePath?: string,
|
|
177
|
+
kubeContext?: string,
|
|
178
|
+
clusterName?: string
|
|
179
|
+
): Promise<void>;
|
|
180
|
+
deleteSkyrampWorker(
|
|
181
|
+
namespace?: string,
|
|
182
|
+
kubePath?: string,
|
|
183
|
+
kubeContext?: string,
|
|
184
|
+
clusterName?: string
|
|
185
|
+
): Promise<void>;
|
|
186
|
+
runDockerSkyrampWorker(
|
|
187
|
+
workerImage?: string,
|
|
188
|
+
workerTag?: string,
|
|
189
|
+
hostPost?: number,
|
|
190
|
+
targetNetworkName?: string
|
|
191
|
+
): Promise<void>;
|
|
192
|
+
removeDockerSkyrampWorker(): Promise<void>;
|
|
152
193
|
mockerApplyV1(options: MockerApplyV1Options): Promise<void>;
|
|
153
|
-
mockerApplyV1(
|
|
154
|
-
|
|
155
|
-
|
|
194
|
+
mockerApplyV1(
|
|
195
|
+
namespace: string,
|
|
196
|
+
kubePath: string,
|
|
197
|
+
kubeContext: string,
|
|
198
|
+
clusterName: string,
|
|
199
|
+
address: string,
|
|
200
|
+
response: ResponseValue | ResponseValue[],
|
|
201
|
+
trafficConfig: TrafficConfig
|
|
202
|
+
): Promise<void>;
|
|
203
|
+
mockerApplyFromFile(
|
|
204
|
+
namespace: string,
|
|
205
|
+
kubePath: string,
|
|
206
|
+
kubeContext: string,
|
|
207
|
+
clusterName: string,
|
|
208
|
+
address: string,
|
|
209
|
+
filePath: string
|
|
210
|
+
): Promise<void>;
|
|
211
|
+
applyMockDescription(
|
|
212
|
+
namespace: string,
|
|
213
|
+
address: string,
|
|
214
|
+
mockYamlContent: string
|
|
215
|
+
): Promise<void>;
|
|
156
216
|
applyMock(mock: MockV2 | MockV2[]): Promise<void>;
|
|
157
|
-
testerStart(
|
|
158
|
-
|
|
217
|
+
testerStart(
|
|
218
|
+
namespace: string,
|
|
219
|
+
kubePath: string,
|
|
220
|
+
kubeContext: string,
|
|
221
|
+
clusterName: string,
|
|
222
|
+
address: string,
|
|
223
|
+
scenario: Scenario
|
|
224
|
+
): Promise<void>;
|
|
225
|
+
testerStartV1(
|
|
226
|
+
namespace: string,
|
|
159
227
|
kubePath: string,
|
|
160
228
|
kubeContext: string,
|
|
161
229
|
clusterName: string,
|
|
162
230
|
address: string,
|
|
163
231
|
scenario: Scenario,
|
|
164
232
|
testName: string,
|
|
165
|
-
globalHeaders: {[headerName: string]: string},
|
|
233
|
+
globalHeaders: { [headerName: string]: string },
|
|
166
234
|
generateTestReport: boolean,
|
|
167
|
-
isDockerenv: boolean
|
|
168
|
-
|
|
235
|
+
isDockerenv: boolean
|
|
236
|
+
): Promise<void>;
|
|
237
|
+
|
|
169
238
|
testerStartV1(options: testerStartV1Options): Promise<void>;
|
|
170
239
|
|
|
171
240
|
testerGenerate(
|
|
@@ -179,9 +248,10 @@ export declare class SkyrampClient {
|
|
|
179
248
|
port: int,
|
|
180
249
|
generateRobot: boolean,
|
|
181
250
|
functionalScenario: boolean,
|
|
182
|
-
negativeScenario: boolean
|
|
251
|
+
negativeScenario: boolean
|
|
252
|
+
): Promise<string[]>;
|
|
183
253
|
|
|
184
|
-
sendRequest(options: SendRequestV2Options): Promise<
|
|
254
|
+
sendRequest(options: SendRequestV2Options): Promise<ResponseV2>;
|
|
185
255
|
deployDashboard(network: string): Promise<void>;
|
|
186
256
|
|
|
187
257
|
generateRestTest(options: GenerateRestTestOptions): Promise<string>;
|
|
@@ -193,5 +263,8 @@ export declare class SkyrampClient {
|
|
|
193
263
|
* @param scenario The scenario object or array of scenarios to run
|
|
194
264
|
* @param options Additional options for the load test
|
|
195
265
|
*/
|
|
196
|
-
sendScenario(
|
|
266
|
+
sendScenario(
|
|
267
|
+
scenario: AsyncScenario | AsyncScenario[],
|
|
268
|
+
options?: SendScenarioOptions
|
|
269
|
+
): Promise<AsyncTestStatus>;
|
|
197
270
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const { expect: playwrightExpect } = require('@playwright/test');
|
|
2
2
|
const lib = require('../lib');
|
|
3
3
|
const koffi = require('koffi');
|
|
4
4
|
|
|
@@ -8,6 +8,9 @@ const responseType = koffi.struct({
|
|
|
8
8
|
});
|
|
9
9
|
|
|
10
10
|
const improvePlaywrightSelectorWrapper = lib.func('improvePlywrightSelectorWrapper', responseType, ['string']);
|
|
11
|
+
const hasJavascriptWrapper = lib.func('hasJavascript', 'bool', ['string']);
|
|
12
|
+
|
|
13
|
+
const defaultWaitForTimeout = 1500
|
|
11
14
|
|
|
12
15
|
async function improvePlaywrightSelectorWithLlm(originalSelector, errorMessage, domContext, pageTitle, pageURL) {
|
|
13
16
|
const requestData = {
|
|
@@ -15,7 +18,8 @@ async function improvePlaywrightSelectorWithLlm(originalSelector, errorMessage,
|
|
|
15
18
|
"error_message": errorMessage,
|
|
16
19
|
"dom_context": domContext,
|
|
17
20
|
"page_title": pageTitle,
|
|
18
|
-
"page_url": pageURL
|
|
21
|
+
"page_url": pageURL,
|
|
22
|
+
"language": "javascript"
|
|
19
23
|
};
|
|
20
24
|
const request = JSON.stringify(requestData);
|
|
21
25
|
|
|
@@ -38,7 +42,6 @@ async function improvePlaywrightSelectorWithLlm(originalSelector, errorMessage,
|
|
|
38
42
|
});
|
|
39
43
|
});
|
|
40
44
|
}
|
|
41
|
-
*/
|
|
42
45
|
|
|
43
46
|
function debug(...args) {
|
|
44
47
|
if (process.env.SKYRAMP_DEBUG == "true") {
|
|
@@ -46,6 +49,165 @@ function debug(...args) {
|
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
function shouldAttemptImprovement(errorMessage, errorName) {
|
|
53
|
+
const improvementKeywords = [
|
|
54
|
+
"timeout",
|
|
55
|
+
"not found",
|
|
56
|
+
"no element",
|
|
57
|
+
"not visible",
|
|
58
|
+
"not attached",
|
|
59
|
+
"selector resolved to hidden",
|
|
60
|
+
"element is not enabled",
|
|
61
|
+
"strict mode violation"
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
if (errorName != "") {
|
|
65
|
+
const playwrightErrorTypes = [
|
|
66
|
+
"TimeoutError",
|
|
67
|
+
"Error",
|
|
68
|
+
"LocatorAssertionError"
|
|
69
|
+
];
|
|
70
|
+
if (playwrightErrorTypes.some(e => e == errorName)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let errorLower = errorMessage.toLowerCase();
|
|
76
|
+
return improvementKeywords.some(keyword => errorLower.includes(keyword));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// for debugging only
|
|
80
|
+
/*
|
|
81
|
+
const sampleSuggestions = [
|
|
82
|
+
{
|
|
83
|
+
selector: "page.getByText('K', { exact: true})",
|
|
84
|
+
confidence: 0.9,
|
|
85
|
+
reasoning: "The 'L' appears to be within a user profile button that shows 'K' as the user initial, using semantic role for better reliability"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
selector: "page.locator('#onyx-user-dropdown')",
|
|
89
|
+
confidence: 0.85,
|
|
90
|
+
reasoning: 'Targets the specific user dropdown container with unique ID that contains the user initial'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
selector: "page.locator('.bg-background-900').filter({ hasText: 'K' })",
|
|
94
|
+
confidence: 0.8,
|
|
95
|
+
reasoning: 'Targets the specific styled element containing the user initial with background class'
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
selector: "page.locator('div').filter({ hasText: /^K$/ }).nth(0)",
|
|
99
|
+
confidence: 0.75,
|
|
100
|
+
reasoning: "Uses regex to find exact 'K' text match and takes first occurrence"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
selector: "page.getByText('kolja').locator('..').getByText('K', { exact: true })",
|
|
104
|
+
confidence: 0.7,
|
|
105
|
+
reasoning: "Navigates to parent of username text and finds the exact 'K' text within that context"
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
function parseErrorStack(ori) {
|
|
111
|
+
const stackLines = ori.split('\n');
|
|
112
|
+
let prev = ""
|
|
113
|
+
for (let idx = stackLines.length - 1; idx >= 0 ; idx --) {
|
|
114
|
+
// this could be improved
|
|
115
|
+
if (stackLines[idx].includes("@skyramp/skyramp")) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
prev = stackLines[idx];
|
|
119
|
+
}
|
|
120
|
+
return prev
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function retryWithLLM(skyrampLocator, error) {
|
|
124
|
+
// when API_KEY is not given, we return right away
|
|
125
|
+
if (!process.env.API_KEY) {
|
|
126
|
+
throw error
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let errorMessage = error.toString();
|
|
130
|
+
let errorType = typeof error;
|
|
131
|
+
|
|
132
|
+
if (!(skyrampLocator instanceof SkyrampPlaywrightLocator)) {
|
|
133
|
+
throw error
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let locatorStr = skyrampLocator._locator.toString();
|
|
137
|
+
|
|
138
|
+
if (!shouldAttemptImprovement(errorMessage, errorType)) {
|
|
139
|
+
debug("cannot improve", error.name);
|
|
140
|
+
throw error
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let pageTitle = await skyrampLocator.page().title();
|
|
144
|
+
let pageUrl = skyrampLocator.page().url();
|
|
145
|
+
|
|
146
|
+
debug("retry with LLM", skyrampLocator._locator);
|
|
147
|
+
|
|
148
|
+
let newMsg = `${errorMessage} (while using selector: ${locatorStr})`;
|
|
149
|
+
|
|
150
|
+
let pageContent = await skyrampLocator.page().content();
|
|
151
|
+
|
|
152
|
+
// for debugging only
|
|
153
|
+
// let suggestions = sampleSuggestions;
|
|
154
|
+
|
|
155
|
+
var suggestions;
|
|
156
|
+
try {
|
|
157
|
+
suggestions = await improvePlaywrightSelectorWithLlm(
|
|
158
|
+
locatorStr,
|
|
159
|
+
newMsg,
|
|
160
|
+
pageContent,
|
|
161
|
+
pageTitle,
|
|
162
|
+
pageUrl
|
|
163
|
+
);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
debug("failed to consult with llm", error)
|
|
166
|
+
throw error
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (suggestions == null || suggestions.length == 0) {
|
|
170
|
+
debug("No LLM suggestoins available, failing with original error");
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
suggestions.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0))
|
|
175
|
+
debug(suggestions)
|
|
176
|
+
|
|
177
|
+
for (let suggestion of suggestions) {
|
|
178
|
+
debug(`try ${suggestion.selector} instead of ${skyrampLocator._locator}`);
|
|
179
|
+
try {
|
|
180
|
+
const newLocator = skyrampLocator.createLocatorFromString(suggestion.selector);
|
|
181
|
+
if (newLocator == null) {
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const locatorCount = await newLocator.count();
|
|
186
|
+
|
|
187
|
+
debug(` trying new Locator ${newLocator}, count = ${locatorCount}`);
|
|
188
|
+
|
|
189
|
+
if (locatorCount == 1) {
|
|
190
|
+
const func = newLocator[skyrampLocator.execFname];
|
|
191
|
+
return func.call(newLocator, skyrampLocator.execParam, skyrampLocator.execArgs).then(result => {
|
|
192
|
+
console.log(`✅ SUCCESS! Used selector: ${newLocator} instead of ${skyrampLocator._locator}`);
|
|
193
|
+
console.log(` ${parseErrorStack(error.stack)}`);
|
|
194
|
+
|
|
195
|
+
skyrampLocator.llmLocator = newLocator;
|
|
196
|
+
skyrampLocator.locatorCount = 1;
|
|
197
|
+
skyrampLocator._skyrampPage.addLLMChoices(skyrampLocator._locator, newLocator, error.stack);
|
|
198
|
+
return result;
|
|
199
|
+
}).catch(error => {
|
|
200
|
+
throw new Error(`retrying with LLM failed at ${skyrampLocator._locator} replacing ${newLocator}`, error);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
throw new Error(`failed to find a working locator from LLM's suggestions`, error);
|
|
209
|
+
}
|
|
210
|
+
|
|
49
211
|
class SkyrampPlaywrightLocator {
|
|
50
212
|
constructor(skyrampPage, locator, prevLocator, args, options, hydration) {
|
|
51
213
|
this._skyrampPage = skyrampPage
|
|
@@ -83,49 +245,29 @@ class SkyrampPlaywrightLocator {
|
|
|
83
245
|
return this._previousLocator && this._previousLocator.isHydration()
|
|
84
246
|
}
|
|
85
247
|
|
|
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
248
|
async execute() {
|
|
124
|
-
debug(`execute ${ this._locator}
|
|
249
|
+
debug(`execute ${ this._locator}.${this.execFname} ${this.execParam ?? ''} ${this.execArgs ?? ''}`)
|
|
125
250
|
const func = this._locator[this.execFname];
|
|
126
251
|
return func.call(this._locator, this.execParam, this.execArgs);
|
|
127
252
|
}
|
|
128
253
|
|
|
254
|
+
generateLLMErrors() {
|
|
255
|
+
let ret = "List of dynamic selectors that could be relevant to error:\n"
|
|
256
|
+
|
|
257
|
+
let choices = this._skyrampPage.getLLMChoices();
|
|
258
|
+
if (choices ==undefined || choices.length == 0) {
|
|
259
|
+
return ""
|
|
260
|
+
}
|
|
261
|
+
for (let i = 0 ; i < choices.length; i ++) {
|
|
262
|
+
const choice = choices[i];
|
|
263
|
+
ret += `${ i+1 }. original locator: ${choice.original}\n`
|
|
264
|
+
ret += ` selected locator: ${choice.new}\n`
|
|
265
|
+
ret += ` ${choice.at}\n`
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return ret;
|
|
269
|
+
}
|
|
270
|
+
|
|
129
271
|
// this is the function that does smart selector retry
|
|
130
272
|
async SmartRetryWithFallback(fname, param, ...args) {
|
|
131
273
|
this.execFname = fname;
|
|
@@ -133,9 +275,12 @@ class SkyrampPlaywrightLocator {
|
|
|
133
275
|
this.execArgs = args;
|
|
134
276
|
|
|
135
277
|
let locatorCount = await this._locator.count();
|
|
136
|
-
debug(`handling ${
|
|
278
|
+
debug(`handling ${ this._locator }.${ fname }, count = ${ locatorCount }`)
|
|
137
279
|
this.locatorCount = locatorCount
|
|
138
280
|
|
|
281
|
+
let currentUrl = this._skyrampPage._page.url();
|
|
282
|
+
debug(`current url = ${currentUrl}`);
|
|
283
|
+
|
|
139
284
|
// if locator exists, DOM is available
|
|
140
285
|
if (locatorCount == 1) {
|
|
141
286
|
// we try action, this will most likely succeed
|
|
@@ -143,50 +288,219 @@ class SkyrampPlaywrightLocator {
|
|
|
143
288
|
return this.execute()
|
|
144
289
|
.catch(error => {
|
|
145
290
|
if (error.name == "TimeoutError") {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
291
|
+
debug(`locator ${this._locator} exists, but execution failed, wait a bit and retry`)
|
|
292
|
+
this.wait(defaultWaitForTimeout)
|
|
293
|
+
|
|
294
|
+
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);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
150
304
|
}
|
|
151
305
|
throw error;
|
|
152
306
|
});
|
|
153
307
|
} else if (locatorCount > 0) {
|
|
154
308
|
// TODO
|
|
155
|
-
debug(
|
|
156
|
-
|
|
309
|
+
debug(`multiple ${locatorCount} locators identified`)
|
|
310
|
+
|
|
311
|
+
if (locatorCount > 5) {
|
|
312
|
+
throw new Error(`${locatorCount} locators detected for ${this._locator}. Please add "data-testid" attribute for a more stable locator`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return this.execute().then(result => {
|
|
316
|
+
return result;
|
|
317
|
+
}).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
|
+
});
|
|
334
|
+
});
|
|
157
335
|
} else {
|
|
158
336
|
// if locator does not exist, we need to consider two cases
|
|
159
337
|
// one is if any actions that required hydration did not work
|
|
160
338
|
// second, if locator id is not correct
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
339
|
+
// check if last step has potential hydration
|
|
340
|
+
if (this._previousLocator && this._previousLocator.locatorCount == 0) {
|
|
341
|
+
debug(`previous action ${this._previousLocator._locator} is potentially associated with hydration`);
|
|
342
|
+
// wait for a short time to finish hydration
|
|
343
|
+
await this.wait(defaultWaitForTimeout);
|
|
344
|
+
|
|
345
|
+
const previousCount = await this._previousLocator.count();
|
|
346
|
+
debug(` re-execute the previous one ${this._previousLocator._locator}, previous Locator count = ${previousCount}`);
|
|
347
|
+
|
|
348
|
+
// re-execute the previous locator
|
|
349
|
+
await this._previousLocator.execute().catch(() => {
|
|
350
|
+
// log the failure but continues to the current one
|
|
351
|
+
debug(`failed to execute previous locator ${this._previousLocator._locator} again`);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
try {
|
|
169
355
|
// then execute the current one
|
|
170
|
-
return this.execute();
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
356
|
+
return await this.execute();
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (error.name == "TimeoutError") {
|
|
359
|
+
debug(`${this._locator} failed at first try. attempting again with some timeout`);
|
|
360
|
+
// wait for some time and re execute
|
|
361
|
+
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
|
+
});
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
throw error;
|
|
175
381
|
}
|
|
176
|
-
}
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
});
|
|
382
|
+
} 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`);
|
|
182
387
|
} else {
|
|
183
|
-
|
|
388
|
+
debug(`${this._locator} locator count is zero, but previous locator seems not related to hydration`);
|
|
389
|
+
}
|
|
390
|
+
// previous action may not be associated with hydration
|
|
391
|
+
// then we just try current locator. could be a locator with a wrong id
|
|
392
|
+
// wait for a short time just in case
|
|
393
|
+
await this.wait(defaultWaitForTimeout);
|
|
394
|
+
|
|
395
|
+
this.locatorCount = await this._locator.count();
|
|
396
|
+
debug(` after waiting locator count = ${this.locatorCount}, url = ${this._skyrampPage._page.url()}`);
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
return await this.execute();
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (error.name == "TimeoutError") {
|
|
402
|
+
debug(`${this._locator} failed at first try. attempting again with some timeout`);
|
|
403
|
+
await this.wait(defaultWaitForTimeout);
|
|
404
|
+
|
|
405
|
+
return this.execute().catch(newError => {
|
|
406
|
+
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;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
184
426
|
throw error;
|
|
185
427
|
}
|
|
186
428
|
}
|
|
187
429
|
}
|
|
188
430
|
}
|
|
189
431
|
|
|
432
|
+
parseFunctionChain(chainStr) {
|
|
433
|
+
// Remove any object prefix like "page."
|
|
434
|
+
chainStr = chainStr.replace(/^[a-zA-Z_][a-zA-Z0-9_]*\./, '');
|
|
435
|
+
|
|
436
|
+
// Match function calls: funcName(args)
|
|
437
|
+
const regex = /([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\)/g;
|
|
438
|
+
let result = [];
|
|
439
|
+
let match;
|
|
440
|
+
while ((match = regex.exec(chainStr)) !== null) {
|
|
441
|
+
const funcName = match[1];
|
|
442
|
+
const argsStr = match[2].trim();
|
|
443
|
+
let args = [];
|
|
444
|
+
if (argsStr.length > 0) {
|
|
445
|
+
// Split arguments by comma, but keep objects and strings together
|
|
446
|
+
// This is a simple approach, for more complex cases use a parser
|
|
447
|
+
let argMatches = [];
|
|
448
|
+
let depth = 0, current = '', inString = false, stringChar = '';
|
|
449
|
+
for (let i = 0; i < argsStr.length; i++) {
|
|
450
|
+
const c = argsStr[i];
|
|
451
|
+
if ((c === "'" || c === '"') && !inString) {
|
|
452
|
+
inString = true;
|
|
453
|
+
stringChar = c;
|
|
454
|
+
current += c;
|
|
455
|
+
} else if (inString && c === stringChar) {
|
|
456
|
+
inString = false;
|
|
457
|
+
current += c;
|
|
458
|
+
} else if (!inString && (c === '{' || c === '[')) {
|
|
459
|
+
depth++;
|
|
460
|
+
current += c;
|
|
461
|
+
} else if (!inString && (c === '}' || c === ']')) {
|
|
462
|
+
depth--;
|
|
463
|
+
current += c;
|
|
464
|
+
} else if (!inString && c === ',' && depth === 0) {
|
|
465
|
+
argMatches.push(current.trim());
|
|
466
|
+
current = '';
|
|
467
|
+
} else {
|
|
468
|
+
current += c;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (current.trim().length > 0) {
|
|
472
|
+
argMatches.push(current.trim());
|
|
473
|
+
}
|
|
474
|
+
args = argMatches;
|
|
475
|
+
}
|
|
476
|
+
result.push({ function: funcName, arguments: args });
|
|
477
|
+
}
|
|
478
|
+
return result;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
createLocatorFromString(suggested) {
|
|
482
|
+
suggested = suggested.trim();
|
|
483
|
+
if (suggested.startsWith("page.")) {
|
|
484
|
+
suggested = suggested.substring(5);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
let chains = this.parseFunctionChain(suggested);
|
|
488
|
+
|
|
489
|
+
let cur = this.page();
|
|
490
|
+
// find locator per function chain
|
|
491
|
+
for (let i = 0; i < chains.length; i ++) {
|
|
492
|
+
const chain = chains[i];
|
|
493
|
+
let f = cur[chain.function];
|
|
494
|
+
let args = [];
|
|
495
|
+
for (const a of chain.arguments) {
|
|
496
|
+
let arg = Function('"use strict";return (' + a + ')')();
|
|
497
|
+
args.push(arg);
|
|
498
|
+
}
|
|
499
|
+
cur = f.call(cur, ...args);
|
|
500
|
+
}
|
|
501
|
+
return cur;
|
|
502
|
+
}
|
|
503
|
+
|
|
190
504
|
async click(...args) {
|
|
191
505
|
return this.SmartRetryWithFallback("click", null, ...args);
|
|
192
506
|
}
|
|
@@ -227,6 +541,55 @@ class SkyrampPlaywrightLocator {
|
|
|
227
541
|
return this.SmartRetryWithFallback("isVisible", null, ...args);
|
|
228
542
|
}
|
|
229
543
|
|
|
544
|
+
filter(options) {
|
|
545
|
+
const originalLocator = this._locator.filter(options);
|
|
546
|
+
return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, null, options);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
locator(selector, options) {
|
|
550
|
+
const originalLocator = this._locator.locator(selector, options);
|
|
551
|
+
return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, selector, options);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
getByRole(role, options) {
|
|
555
|
+
const originalLocator = this._locator.getByRole(role, options);
|
|
556
|
+
return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, role, options);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
getByText(text, options) {
|
|
560
|
+
const originalLocator = this._locator.getByText(text, options);
|
|
561
|
+
return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, text, options);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
getByLabel(label, options) {
|
|
565
|
+
const originalLocator = this._locator.getByLabel(label, options);
|
|
566
|
+
return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, label, options);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
getByTestId(testId, options) {
|
|
570
|
+
const originalLocator = this._locator.getByTestId(testId);
|
|
571
|
+
return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, testId, options);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
getByTitle(title, options) {
|
|
575
|
+
const originalLocator = this._locator.getByTitle(title, options);
|
|
576
|
+
return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, title, options);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
getByPlaceholder(placeholder, options) {
|
|
580
|
+
const originalLocator = this._locator.getByPlaceholder(placeholder, options);
|
|
581
|
+
return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, placeholder, options);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
getByAltText(alt, options) {
|
|
585
|
+
const originalLocator = this._locator.getByAltText(alt, options);
|
|
586
|
+
return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, alt, options);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
unwrap() {
|
|
590
|
+
return this._locator
|
|
591
|
+
}
|
|
592
|
+
|
|
230
593
|
nth(index) {
|
|
231
594
|
// get nth element - return a new SkyrampPlaywrightLocator
|
|
232
595
|
let new_locator = this._locator.nth(index)
|
|
@@ -244,107 +607,173 @@ class SkyrampPlaywrightLocator {
|
|
|
244
607
|
let new_locator = this._locator.last()
|
|
245
608
|
return this._skyrampPage.newSkyrampPlaywrightLocator(new_locator, null, null);
|
|
246
609
|
}
|
|
610
|
+
|
|
611
|
+
page() {
|
|
612
|
+
return this._skyrampPage._page;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async wait(t) {
|
|
616
|
+
debug(`wait for ${t}`);
|
|
617
|
+
return this._skyrampPage._page.waitForTimeout(t);
|
|
618
|
+
}
|
|
247
619
|
}
|
|
248
620
|
|
|
249
621
|
class SkyrampPlaywrightPage {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
622
|
+
constructor(page) {
|
|
623
|
+
this._page = page;
|
|
624
|
+
this._curUrl = "";
|
|
625
|
+
return new Proxy(this, {
|
|
626
|
+
// The `get` trap is the key to forwarding.
|
|
627
|
+
// This will foraward any methods not implemented in this struct
|
|
628
|
+
// to be handled by the original class (i.e., playwright page object)
|
|
629
|
+
get(wrapper, prop, receiver) {
|
|
630
|
+
// First, check if the property exists on the wrapper.
|
|
631
|
+
if (Reflect.has(wrapper, prop)) {
|
|
632
|
+
return Reflect.get(wrapper, prop, receiver);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Otherwise, forward the call to the original object (`target`).
|
|
636
|
+
const value = Reflect.get(wrapper._page, prop, wrapper._page);
|
|
637
|
+
|
|
638
|
+
// If the property is a function, bind it to the original object.
|
|
639
|
+
// This ensures the correct `this` context is used when the method is called.
|
|
640
|
+
if (typeof value === 'function') {
|
|
641
|
+
return value.bind(wrapper._page);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return value;
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
getCurUrl() {
|
|
650
|
+
return this._curUrl;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
pushLocator(locator) {
|
|
654
|
+
if (this.locators == undefined ) {
|
|
655
|
+
this.locators = [];
|
|
260
656
|
}
|
|
657
|
+
this.locators.push(locator);
|
|
658
|
+
}
|
|
261
659
|
|
|
262
|
-
|
|
263
|
-
|
|
660
|
+
getLastLocator() {
|
|
661
|
+
if (this.locators == undefined) {
|
|
662
|
+
return null
|
|
663
|
+
}
|
|
264
664
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (typeof value === 'function') {
|
|
268
|
-
return value.bind(wrapper._page);
|
|
665
|
+
if (this.locators.length == 0) {
|
|
666
|
+
return null
|
|
269
667
|
}
|
|
668
|
+
return this.locators.at(-1)
|
|
669
|
+
}
|
|
270
670
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
671
|
+
newSkyrampPlaywrightLocator(originalLocator, param, options) {
|
|
672
|
+
let prevLocator = this.getLastLocator();
|
|
673
|
+
const hydration = options && (options.hydration || false);
|
|
674
|
+
/*
|
|
675
|
+
if (prevLocator != null) {
|
|
676
|
+
debug(`handling ${originalLocator} prev ${prevLocator._locator}`);
|
|
677
|
+
} else {
|
|
678
|
+
debug(`handling ${originalLocator}`);
|
|
679
|
+
}
|
|
680
|
+
*/
|
|
275
681
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
this.locators.push(locator);
|
|
281
|
-
}
|
|
682
|
+
let newLocator = new SkyrampPlaywrightLocator(this, originalLocator, prevLocator, [param], options, hydration);
|
|
683
|
+
this.pushLocator(newLocator)
|
|
684
|
+
return newLocator
|
|
685
|
+
}
|
|
282
686
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
687
|
+
locator(selector, options) {
|
|
688
|
+
const originalLocator = this._page.locator(selector, options);
|
|
689
|
+
return this.newSkyrampPlaywrightLocator(originalLocator, selector, options);
|
|
690
|
+
}
|
|
287
691
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
692
|
+
getByRole(role, options) {
|
|
693
|
+
const originalLocator = this._page.getByRole(role, options);
|
|
694
|
+
return this.newSkyrampPlaywrightLocator(originalLocator, role, options);
|
|
695
|
+
}
|
|
293
696
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
let newLocator = new SkyrampPlaywrightLocator(this, originalLocator, prevLocator, [param], options, hydration);
|
|
299
|
-
this.pushLocator(newLocator)
|
|
300
|
-
return newLocator
|
|
301
|
-
}
|
|
697
|
+
getByText(text, options) {
|
|
698
|
+
const originalLocator = this._page.getByText(text, options);
|
|
699
|
+
return this.newSkyrampPlaywrightLocator(originalLocator, text, options);
|
|
700
|
+
}
|
|
302
701
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
702
|
+
getByLabel(label, options) {
|
|
703
|
+
const originalLocator = this._page.getByLabel(label, options);
|
|
704
|
+
return this.newSkyrampPlaywrightLocator(originalLocator, label, options);
|
|
705
|
+
}
|
|
307
706
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
707
|
+
getByTestId(testId, options) {
|
|
708
|
+
const originalLocator = this._page.getByTestId(testId);
|
|
709
|
+
return this.newSkyrampPlaywrightLocator(originalLocator, testId, options);
|
|
710
|
+
}
|
|
312
711
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
712
|
+
getByTitle(title, options) {
|
|
713
|
+
const originalLocator = this._page.getByTitle(title, options);
|
|
714
|
+
return this.newSkyrampPlaywrightLocator(originalLocator, title, options);
|
|
715
|
+
}
|
|
317
716
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
717
|
+
getByPlaceholder(placeholder, options) {
|
|
718
|
+
const originalLocator = this._page.getByPlaceholder(placeholder, options);
|
|
719
|
+
return this.newSkyrampPlaywrightLocator(originalLocator, placeholder, options);
|
|
720
|
+
}
|
|
322
721
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
722
|
+
getByAltText(alt, options) {
|
|
723
|
+
const originalLocator = this._page.getByAltText(alt, options);
|
|
724
|
+
return this.newSkyrampPlaywrightLocator(originalLocator, alt, options);
|
|
725
|
+
}
|
|
327
726
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
727
|
+
async goto(url, options) {
|
|
728
|
+
const result = await this._page.goto(url, options);
|
|
729
|
+
const content = await this._page.content();
|
|
730
|
+
if (hasJavascriptWrapper(content)) {
|
|
731
|
+
debug(`javascript download detected when visiting ${this._page.url()}`);
|
|
732
|
+
debug(` wait for sometime for potential hydration`);
|
|
733
|
+
await this._page.waitForTimeout(defaultWaitForTimeout);
|
|
734
|
+
} else {
|
|
735
|
+
debug(`javascript not detected when visiting ${this._page.url()}`);
|
|
736
|
+
}
|
|
737
|
+
this._curUrl = this._page.url();
|
|
738
|
+
return result;
|
|
739
|
+
}
|
|
332
740
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
741
|
+
addLLMChoices(originalLocator, newLocator, stack) {
|
|
742
|
+
if (this.llmChoices == undefined) {
|
|
743
|
+
this.llmChoices = []
|
|
744
|
+
}
|
|
337
745
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
746
|
+
this.llmChoices.push({
|
|
747
|
+
"original": originalLocator.toString(),
|
|
748
|
+
"new": newLocator.toString(),
|
|
749
|
+
"at": parseErrorStack(stack),
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
getLLMChoices() {
|
|
754
|
+
return this.llmChoices
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
hasLLMChoices() {
|
|
758
|
+
if (this.llmChoices == undefined || this.llmChoices.length == 0) {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
342
763
|
}
|
|
343
764
|
|
|
344
765
|
function newSkyrampPlaywrightPage(page) {
|
|
345
766
|
return new SkyrampPlaywrightPage(page);
|
|
346
767
|
}
|
|
347
768
|
|
|
769
|
+
function expect(obj) {
|
|
770
|
+
if (obj instanceof SkyrampPlaywrightLocator) {
|
|
771
|
+
return playwrightExpect(obj.unwrap());
|
|
772
|
+
}
|
|
773
|
+
return playwrightExpect(obj);
|
|
774
|
+
}
|
|
775
|
+
|
|
348
776
|
module.exports = {
|
|
349
777
|
newSkyrampPlaywrightPage,
|
|
778
|
+
expect,
|
|
350
779
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ResponseV2 } from "./classes/ResponseV2";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if the response's status code matches the expected status code (with support for wildcards).
|
|
5
|
+
*
|
|
6
|
+
* The expected status can include 'x' as a wildcard for a single digit.
|
|
7
|
+
* Examples:
|
|
8
|
+
* - "200" matches exactly 200
|
|
9
|
+
* - "20x" matches any status code in the range 200 to 209
|
|
10
|
+
* - "2xx" matches any status code in the range 200 to 299
|
|
11
|
+
*
|
|
12
|
+
* @param response - The response object containing a `status_code` attribute.
|
|
13
|
+
* @param expectedStatus - The expected status code as a string with optional 'x' wildcards.
|
|
14
|
+
* @returns True if the status code matches, false otherwise.
|
|
15
|
+
* @throws Error if the response object lacks a 'status_code' attribute.
|
|
16
|
+
*/
|
|
17
|
+
export function checkStatusCode(
|
|
18
|
+
response: ResponseV2,
|
|
19
|
+
expectedStatus: string
|
|
20
|
+
): boolean;
|
package/src/index.d.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export * from
|
|
6
|
-
export * from
|
|
7
|
-
export * from
|
|
8
|
-
export * from
|
|
9
|
-
export * from
|
|
10
|
-
export * from
|
|
11
|
-
export * from
|
|
12
|
-
export * from
|
|
13
|
-
export * from
|
|
14
|
-
export * from
|
|
15
|
-
export * from
|
|
16
|
-
export * from
|
|
17
|
-
export * from
|
|
18
|
-
export * from
|
|
19
|
-
export * from
|
|
20
|
-
export * from
|
|
21
|
-
export * from
|
|
1
|
+
export * from "./classes/Endpoint";
|
|
2
|
+
export * from "./classes/GrpcEndpoint";
|
|
3
|
+
export * from "./classes/RestEndpoint";
|
|
4
|
+
export * from "./classes/Scenario";
|
|
5
|
+
export * from "./classes/SkyrampClient";
|
|
6
|
+
export * from "./classes/RequestValue";
|
|
7
|
+
export * from "./classes/ResponseValue";
|
|
8
|
+
export * from "./classes/RestParam";
|
|
9
|
+
export * from "./classes/MultipartParam";
|
|
10
|
+
export * from "./classes/TrafficConfig";
|
|
11
|
+
export * from "./classes/DelayConfig";
|
|
12
|
+
export * from "./classes/Protocol";
|
|
13
|
+
export * from "./classes/RequestV2";
|
|
14
|
+
export * from "./classes/ResponseV2";
|
|
15
|
+
export * from "./classes/MockV2";
|
|
16
|
+
export * from "./classes/AsyncScenario";
|
|
17
|
+
export * from "./classes/LoadTestConfig";
|
|
18
|
+
export * from "./classes/AsyncTestStatus";
|
|
19
|
+
export * from "./utils";
|
|
20
|
+
export * from "./function";
|
|
21
|
+
export * from "./classes/SmartPlaywright";
|
package/src/index.js
CHANGED
|
@@ -19,7 +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
|
+
const { newSkyrampPlaywrightPage, expect } = require('./classes/SmartPlaywright');
|
|
23
23
|
|
|
24
24
|
module.exports = {
|
|
25
25
|
SkyrampClient,
|
|
@@ -47,4 +47,5 @@ module.exports = {
|
|
|
47
47
|
checkSchema,
|
|
48
48
|
iterate,
|
|
49
49
|
newSkyrampPlaywrightPage,
|
|
50
|
+
expect,
|
|
50
51
|
}
|
package/src/utils.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if the given value is a JSON string.
|
|
3
|
+
* @param value - The value to check
|
|
4
|
+
* @returns True if the value is a JSON string, false otherwise
|
|
5
|
+
*/
|
|
6
|
+
export function isJSONString(value: string): boolean;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Safely converts a value to a JSON string.
|
|
10
|
+
* @param value - The value to stringify
|
|
11
|
+
* @returns The JSON string representation
|
|
12
|
+
*/
|
|
13
|
+
export function safeStringify(value: string): string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a test description from a scenario.
|
|
17
|
+
* @param scenario - The scenario object
|
|
18
|
+
* @returns The test description
|
|
19
|
+
*/
|
|
20
|
+
export function createTestDescriptionFromScenario(scenario: object): object;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Gets YAML bytes from a configuration.
|
|
24
|
+
* @param config - The configuration object
|
|
25
|
+
* @returns The YAML bytes
|
|
26
|
+
*/
|
|
27
|
+
export function getYamlBytes(config: object): string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reads data from a file.
|
|
31
|
+
* @param filePath - The path to the file
|
|
32
|
+
* @returns The file data
|
|
33
|
+
*/
|
|
34
|
+
export function readDataFromFile(
|
|
35
|
+
filename: string
|
|
36
|
+
): [string, false] | [string, true];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extracts a value from JSON using JSONPath syntax.
|
|
40
|
+
* @param jsonInput - The JSON object or string to extract from
|
|
41
|
+
* @param path - The JSONPath expression (e.g., "$.value", "$.data[0].name")
|
|
42
|
+
* @returns The extracted value or null if not found
|
|
43
|
+
*/
|
|
44
|
+
export function getValue(
|
|
45
|
+
jsonInput: object,
|
|
46
|
+
path: string
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
+
): any;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validates JSON against a schema.
|
|
52
|
+
* @param jsonInput - The JSON to validate
|
|
53
|
+
* @param schema - The schema to validate against
|
|
54
|
+
* @returns The validation result
|
|
55
|
+
*/
|
|
56
|
+
export function checkSchema(jsonInput: object, schema: object): boolean;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Iterates over JSON data structure.
|
|
60
|
+
* @param jsonInput - The JSON data to iterate
|
|
61
|
+
* @returns The iteration result
|
|
62
|
+
*/
|
|
63
|
+
export function iterate(jsonInput: object): object;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The Skyramp YAML version constant.
|
|
67
|
+
*/
|
|
68
|
+
export const SKYRAMP_YAML_VERSION: string;
|