@midscene/web 0.0.1 → 0.1.1
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/dist/es/index.js +342 -114
- package/dist/es/playwright-report.js +2380 -0
- package/dist/lib/index.js +343 -114
- package/dist/lib/playwright-report.js +2383 -0
- package/dist/script/htmlElement.js +596 -25
- package/dist/script/types/htmlElement.d.ts +2 -0
- package/dist/types/index.d.ts +116 -21
- package/dist/types/playwright-report.d.ts +10 -0
- package/package.json +25 -4
- package/modern.config.ts +0 -13
- package/modern.inspect.config.ts +0 -20
- package/playwright.config.ts +0 -42
- package/src/html-element/constants.ts +0 -10
- package/src/html-element/debug.ts +0 -3
- package/src/html-element/dom-util.ts +0 -11
- package/src/html-element/extractInfo.ts +0 -168
- package/src/html-element/index.ts +0 -1
- package/src/html-element/util.ts +0 -160
- package/src/img/img.ts +0 -132
- package/src/img/util.ts +0 -28
- package/src/index.ts +0 -2
- package/src/playwright/actions.ts +0 -276
- package/src/playwright/cdp.ts +0 -322
- package/src/playwright/element.ts +0 -74
- package/src/playwright/index.ts +0 -120
- package/src/playwright/utils.ts +0 -88
- package/src/puppeteer/element.ts +0 -49
- package/src/puppeteer/index.ts +0 -6
- package/src/puppeteer/utils.ts +0 -116
- package/tests/e2e/ai-auto-todo.spec.ts +0 -24
- package/tests/e2e/ai-xicha.spec.ts +0 -34
- package/tests/e2e/fixture.ts +0 -6
- package/tests/e2e/generate-test-data.spec.ts +0 -60
- package/tests/e2e/todo-app-midscene.spec.ts +0 -98
- package/tests/e2e/tool.ts +0 -63
- package/tsconfig.json +0 -23
- package/vitest.config.ts +0 -14
package/src/playwright/cdp.ts
DELETED
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
// fork from https://github.com/zerostep-ai/zerostep/blob/5ba8ca5282879f444e5dfefbcc1f03c76591469e/packages/playwright/src/cdp.ts
|
|
2
|
-
import type { Page } from 'playwright';
|
|
3
|
-
|
|
4
|
-
const cdpSessionByPage = new Map<Page, CDPSession>();
|
|
5
|
-
|
|
6
|
-
export const WEBDRIVER_ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf';
|
|
7
|
-
|
|
8
|
-
export type ScrollType = 'up' | 'down' | 'bottom' | 'top';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Closes the cdp session and clears the global shared reference. This
|
|
12
|
-
* happens automatically when a page closes in a playwright test, so
|
|
13
|
-
* should generally not be necessary.
|
|
14
|
-
*/
|
|
15
|
-
export const detachCPDSession = async (page: Page) => {
|
|
16
|
-
if (cdpSessionByPage.has(page)) {
|
|
17
|
-
await cdpSessionByPage.get(page)!.detach();
|
|
18
|
-
cdpSessionByPage.delete(page);
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Returns a stable reference to a CDP session.
|
|
24
|
-
*/
|
|
25
|
-
export const getCDPSession = async (page: Page): Promise<CDPSession> => {
|
|
26
|
-
if (!cdpSessionByPage.has(page)) {
|
|
27
|
-
try {
|
|
28
|
-
const session = await page.context().newCDPSession(page);
|
|
29
|
-
cdpSessionByPage.set(page, session);
|
|
30
|
-
} catch (e) {
|
|
31
|
-
throw Error('The ai() function can only be run against Chromium browsers.');
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return cdpSessionByPage.get(page)!;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export const getScreenshot = async (page: Page) => {
|
|
39
|
-
const cdpSession = await getCDPSession(page);
|
|
40
|
-
const screenshot = await cdpSession.send('Page.captureScreenshot');
|
|
41
|
-
return screenshot.data; // Base64-encoded image data
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export const scrollIntoView = async (page: Page, args: { id: string }) => {
|
|
45
|
-
const cdpSession = await getCDPSession(page);
|
|
46
|
-
|
|
47
|
-
await cdpSession.send('DOM.scrollIntoViewIfNeeded', {
|
|
48
|
-
backendNodeId: parseInt(args.id, 10),
|
|
49
|
-
});
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export const getTitle = async (page: Page) => {
|
|
53
|
-
const cdpSession = await getCDPSession(page);
|
|
54
|
-
const returnedValue = await cdpSession.send('Runtime.evaluate', {
|
|
55
|
-
expression: 'document.title',
|
|
56
|
-
returnByValue: true,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
return returnedValue.result.value;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export const get = async (page: Page, args: { url: string }) => {
|
|
63
|
-
const cdpSession = await getCDPSession(page);
|
|
64
|
-
await cdpSession.send('Page.navigate', {
|
|
65
|
-
url: args.url,
|
|
66
|
-
});
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export const scrollElement = async (page: Page, args: { id: string; target: ScrollType }) => {
|
|
70
|
-
await runFunctionOn(page, {
|
|
71
|
-
functionDeclaration: `function() {
|
|
72
|
-
let element = this
|
|
73
|
-
let elementHeight = 0
|
|
74
|
-
|
|
75
|
-
switch (element.tagName) {
|
|
76
|
-
case 'BODY':
|
|
77
|
-
case 'HTML':
|
|
78
|
-
element = document.scrollingElement || document.body
|
|
79
|
-
elementHeight = window.visualViewport?.height ?? 720
|
|
80
|
-
break
|
|
81
|
-
default:
|
|
82
|
-
elementHeight = element.clientHeight ?? 720
|
|
83
|
-
break
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const relativeScrollDistance = 0.75 * elementHeight
|
|
87
|
-
|
|
88
|
-
switch ("${args.target}") {
|
|
89
|
-
case 'top':
|
|
90
|
-
return element.scrollTo({ top: 0 })
|
|
91
|
-
case 'bottom':
|
|
92
|
-
return element.scrollTo({ top: element.scrollHeight })
|
|
93
|
-
case 'up':
|
|
94
|
-
return element.scrollBy({ top: -relativeScrollDistance })
|
|
95
|
-
case 'down':
|
|
96
|
-
return element.scrollBy({ top: relativeScrollDistance })
|
|
97
|
-
default:
|
|
98
|
-
throw Error('Unsupported scroll target ${args.target}')
|
|
99
|
-
}
|
|
100
|
-
}`,
|
|
101
|
-
backendNodeId: parseInt(args.id, 10),
|
|
102
|
-
});
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
export const runFunctionOn = async (
|
|
106
|
-
page: Page,
|
|
107
|
-
args: { functionDeclaration: string; backendNodeId: number },
|
|
108
|
-
) => {
|
|
109
|
-
const cdpSession = await getCDPSession(page);
|
|
110
|
-
const {
|
|
111
|
-
object: { objectId },
|
|
112
|
-
} = await cdpSession.send('DOM.resolveNode', { backendNodeId: args.backendNodeId });
|
|
113
|
-
await cdpSession.send('Runtime.callFunctionOn', {
|
|
114
|
-
functionDeclaration: args.functionDeclaration,
|
|
115
|
-
objectId,
|
|
116
|
-
});
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
export const clearElement = async (page: Page, args: { id: string }) => {
|
|
120
|
-
return await runFunctionOn(page, {
|
|
121
|
-
functionDeclaration: `function() {this.value=''}`,
|
|
122
|
-
backendNodeId: parseInt(args.id, 10),
|
|
123
|
-
});
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
export const sendKeysToElement = async (page: Page, args: { id: string; value: string[] }) => {
|
|
127
|
-
const cdpSession = await getCDPSession(page);
|
|
128
|
-
const value = args.value[0];
|
|
129
|
-
|
|
130
|
-
const { nodeId } = await cdpSession.send('DOM.requestNode', { objectId: args.id });
|
|
131
|
-
await cdpSession.send('DOM.focus', { nodeId });
|
|
132
|
-
|
|
133
|
-
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
|
134
|
-
for (let i = 0; i < value.length; i++) {
|
|
135
|
-
await cdpSession.send('Input.dispatchKeyEvent', {
|
|
136
|
-
type: 'char',
|
|
137
|
-
text: value[i],
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return true;
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
export const getElementAttribute = async (page: Page, args: { id: string; name: string }) => {
|
|
145
|
-
const cdpSession = await getCDPSession(page);
|
|
146
|
-
|
|
147
|
-
const { nodeId } = await cdpSession.send('DOM.requestNode', { objectId: args.id });
|
|
148
|
-
const { attributes } = await cdpSession.send('DOM.getAttributes', { nodeId });
|
|
149
|
-
|
|
150
|
-
for (let i = 0; i < attributes.length; i++) {
|
|
151
|
-
if (attributes[i] === args.name) {
|
|
152
|
-
return attributes[i + 1];
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return attributes;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
export const getElementTagName = async (page: Page, args: { id: string }) => {
|
|
159
|
-
const cdpSession = await getCDPSession(page);
|
|
160
|
-
const returnedValue = await cdpSession.send('Runtime.callFunctionOn', {
|
|
161
|
-
functionDeclaration: `function() {return this.tagName}`,
|
|
162
|
-
objectId: args.id,
|
|
163
|
-
returnByValue: true,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
return returnedValue.result.value;
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
export const clickElement = async (page: Page, args: { id: string }) => {
|
|
170
|
-
const cdpSession = await getCDPSession(page);
|
|
171
|
-
const { centerX, centerY } = await getContentQuads(page, { backendNodeId: parseInt(args.id, 10) });
|
|
172
|
-
|
|
173
|
-
await cdpSession.send('Input.dispatchMouseEvent', {
|
|
174
|
-
type: 'mousePressed',
|
|
175
|
-
x: centerX,
|
|
176
|
-
y: centerY,
|
|
177
|
-
button: 'left',
|
|
178
|
-
clickCount: 1,
|
|
179
|
-
buttons: 1,
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
await cdpSession.send('Input.dispatchMouseEvent', {
|
|
183
|
-
type: 'mouseReleased',
|
|
184
|
-
x: centerX,
|
|
185
|
-
y: centerY,
|
|
186
|
-
button: 'left',
|
|
187
|
-
clickCount: 1,
|
|
188
|
-
buttons: 1,
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
return true;
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
export const getContentQuads = async (page: Page, args: { backendNodeId: number }) => {
|
|
195
|
-
const cdpSession = await getCDPSession(page);
|
|
196
|
-
const quadsResponse = await cdpSession.send('DOM.getContentQuads', args);
|
|
197
|
-
|
|
198
|
-
const [topLeftX, topLeftY, topRightX, topRightY, bottomRightX, bottomRightY, bottomLeftX, bottomLeftY] =
|
|
199
|
-
quadsResponse.quads[0];
|
|
200
|
-
|
|
201
|
-
const width = topRightX - topLeftX;
|
|
202
|
-
const height = bottomRightY - topRightY;
|
|
203
|
-
const centerX = topLeftX + width / 2;
|
|
204
|
-
const centerY = topRightY + height / 2;
|
|
205
|
-
|
|
206
|
-
console.log('getContentQuads', args, {
|
|
207
|
-
width,
|
|
208
|
-
height,
|
|
209
|
-
topLeftX,
|
|
210
|
-
topLeftY,
|
|
211
|
-
centerX,
|
|
212
|
-
centerY,
|
|
213
|
-
});
|
|
214
|
-
return {
|
|
215
|
-
topLeftX,
|
|
216
|
-
topLeftY,
|
|
217
|
-
topRightX,
|
|
218
|
-
topRightY,
|
|
219
|
-
bottomRightX,
|
|
220
|
-
bottomRightY,
|
|
221
|
-
bottomLeftX,
|
|
222
|
-
bottomLeftY,
|
|
223
|
-
width,
|
|
224
|
-
height,
|
|
225
|
-
centerX,
|
|
226
|
-
centerY,
|
|
227
|
-
};
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
export const focusElement = async (page: Page, args: { backendNodeId: number }) => {
|
|
231
|
-
const cdpSession = await getCDPSession(page);
|
|
232
|
-
await cdpSession.send('DOM.focus', args);
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
export const getElementRect = async (page: Page, args: { id: string }) => {
|
|
236
|
-
const cdpSession = await getCDPSession(page);
|
|
237
|
-
const returnedValue = await cdpSession.send('Runtime.callFunctionOn', {
|
|
238
|
-
functionDeclaration: `function() {return JSON.parse(JSON.stringify(this.getBoundingClientRect()))}`,
|
|
239
|
-
objectId: args.id,
|
|
240
|
-
returnByValue: true,
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
return returnedValue.result.value;
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
export const findElements = async (page: Page, args: { using: string; value: string }) => {
|
|
247
|
-
switch (args.using) {
|
|
248
|
-
case 'css selector':
|
|
249
|
-
case 'tag name':
|
|
250
|
-
return await querySelectorAll(page, { selector: args.value });
|
|
251
|
-
default:
|
|
252
|
-
throw Error(`Unsupported findElements strategy ${args.using}`);
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
export const querySelectorAll = async (page: Page, args: { selector: string }) => {
|
|
257
|
-
const cdpSession = await getCDPSession(page);
|
|
258
|
-
const rootDocumentNode = await cdpSession.send('DOM.getDocument', { depth: -1 });
|
|
259
|
-
const returned = await cdpSession.send('DOM.querySelectorAll', {
|
|
260
|
-
nodeId: rootDocumentNode.root.nodeId,
|
|
261
|
-
selector: args.selector,
|
|
262
|
-
});
|
|
263
|
-
const resolvedNodesPromises = returned.nodeIds.map(
|
|
264
|
-
async (nodeId) => await cdpSession.send('DOM.resolveNode', { nodeId }),
|
|
265
|
-
);
|
|
266
|
-
const resolvedNodes = await Promise.all(resolvedNodesPromises);
|
|
267
|
-
const returnValue = resolvedNodes.map((node) => ({ [WEBDRIVER_ELEMENT_KEY]: node.object.objectId }));
|
|
268
|
-
return returnValue;
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
export const getCurrentUrl = async (page: Page) => {
|
|
272
|
-
const cdpSession = await getCDPSession(page);
|
|
273
|
-
const returned = await cdpSession.send('Page.getNavigationHistory');
|
|
274
|
-
const returnValue = returned.entries[returned.currentIndex].url;
|
|
275
|
-
return returnValue;
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
export const executeScript = async (page: Page, args: { script: string; args: any[] }) => {
|
|
279
|
-
const functionDeclaration = `function() { ${args.script} }`;
|
|
280
|
-
const functionArgs = args.args.map((arg) => {
|
|
281
|
-
if (typeof arg === 'boolean' || typeof arg === 'string' || typeof arg === 'number') {
|
|
282
|
-
return { value: arg };
|
|
283
|
-
} else if (arg && typeof arg === 'object' && Reflect.has(arg, WEBDRIVER_ELEMENT_KEY)) {
|
|
284
|
-
return { objectId: arg[WEBDRIVER_ELEMENT_KEY] };
|
|
285
|
-
} else {
|
|
286
|
-
return { value: undefined };
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
const cdpSession = await getCDPSession(page);
|
|
291
|
-
await cdpSession.send('Runtime.enable');
|
|
292
|
-
const window = await cdpSession.send('Runtime.evaluate', { expression: 'window' });
|
|
293
|
-
|
|
294
|
-
const returnedRef = await cdpSession.send('Runtime.callFunctionOn', {
|
|
295
|
-
objectId: window.result.objectId,
|
|
296
|
-
functionDeclaration,
|
|
297
|
-
arguments: functionArgs,
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
if (returnedRef.result.className === 'NodeList') {
|
|
301
|
-
const nodeProperties = await cdpSession.send('Runtime.getProperties', {
|
|
302
|
-
objectId: returnedRef.result.objectId!,
|
|
303
|
-
ownProperties: true,
|
|
304
|
-
});
|
|
305
|
-
return nodeProperties.result
|
|
306
|
-
.map((e: any) => (!isNaN(parseInt(e.name, 10)) ? { [WEBDRIVER_ELEMENT_KEY]: e.value?.objectId } : null))
|
|
307
|
-
.filter((e: any) => e);
|
|
308
|
-
} else if (returnedRef.result.className === 'HTMLHtmlElement') {
|
|
309
|
-
return { [WEBDRIVER_ELEMENT_KEY]: returnedRef.result.objectId };
|
|
310
|
-
} else {
|
|
311
|
-
const returnedValue = await cdpSession.send('Runtime.callFunctionOn', {
|
|
312
|
-
objectId: window.result.objectId,
|
|
313
|
-
functionDeclaration,
|
|
314
|
-
arguments: functionArgs,
|
|
315
|
-
returnByValue: true,
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
return returnedValue.result.value;
|
|
319
|
-
}
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
type CDPSession = Awaited<ReturnType<ReturnType<Page['context']>['newCDPSession']>>;
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { Page } from 'playwright';
|
|
2
|
-
import { BaseElement, Rect } from '@midscene/core';
|
|
3
|
-
import { NodeType } from '../html-element/constants';
|
|
4
|
-
|
|
5
|
-
export interface WebElementInfoType extends BaseElement {
|
|
6
|
-
id: string;
|
|
7
|
-
locator: string;
|
|
8
|
-
attributes: {
|
|
9
|
-
['nodeType']: NodeType;
|
|
10
|
-
[key: string]: string;
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class WebElementInfo implements BaseElement {
|
|
15
|
-
content: string;
|
|
16
|
-
|
|
17
|
-
locator: string;
|
|
18
|
-
|
|
19
|
-
rect: Rect;
|
|
20
|
-
|
|
21
|
-
center: [number, number];
|
|
22
|
-
|
|
23
|
-
page: Page;
|
|
24
|
-
|
|
25
|
-
id: string;
|
|
26
|
-
|
|
27
|
-
attributes: {
|
|
28
|
-
['nodeType']: NodeType;
|
|
29
|
-
[key: string]: string;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
constructor({
|
|
33
|
-
content,
|
|
34
|
-
rect,
|
|
35
|
-
page,
|
|
36
|
-
locator,
|
|
37
|
-
id,
|
|
38
|
-
attributes,
|
|
39
|
-
}: {
|
|
40
|
-
content: string;
|
|
41
|
-
rect: Rect;
|
|
42
|
-
page: Page;
|
|
43
|
-
locator: string;
|
|
44
|
-
id: string;
|
|
45
|
-
attributes: {
|
|
46
|
-
['nodeType']: NodeType;
|
|
47
|
-
[key: string]: string;
|
|
48
|
-
};
|
|
49
|
-
}) {
|
|
50
|
-
this.content = content;
|
|
51
|
-
this.rect = rect;
|
|
52
|
-
this.center = [Math.floor(rect.left + rect.width / 2), Math.floor(rect.top + rect.height / 2)];
|
|
53
|
-
this.page = page;
|
|
54
|
-
this.locator = locator;
|
|
55
|
-
this.id = id;
|
|
56
|
-
this.attributes = attributes;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async tap() {
|
|
60
|
-
await this.page.mouse.click(this.center[0], this.center[1]);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async hover() {
|
|
64
|
-
await this.page.mouse.move(this.center[0], this.center[1]);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async type(text: string) {
|
|
68
|
-
await this.page.keyboard.type(text);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async press(key: Parameters<typeof this.page.keyboard.press>[0]) {
|
|
72
|
-
await this.page.keyboard.press(key);
|
|
73
|
-
}
|
|
74
|
-
}
|
package/src/playwright/index.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { TestInfo, TestType } from '@playwright/test';
|
|
2
|
-
import { ExecutionDump, GroupedActionDump } from '@midscene/core';
|
|
3
|
-
import { groupedActionDumpFileExt, writeDumpFile } from '@midscene/core/utils';
|
|
4
|
-
import { PlayWrightActionAgent } from './actions';
|
|
5
|
-
|
|
6
|
-
export { PlayWrightActionAgent } from './actions';
|
|
7
|
-
|
|
8
|
-
export type APITestType = Pick<TestType<any, any>, 'step'>;
|
|
9
|
-
|
|
10
|
-
export const PlaywrightAiFixture = () => {
|
|
11
|
-
const dumps: GroupedActionDump[] = [];
|
|
12
|
-
|
|
13
|
-
const appendDump = (groupName: string, execution: ExecutionDump) => {
|
|
14
|
-
let currentDump = dumps.find((dump) => dump.groupName === groupName);
|
|
15
|
-
if (!currentDump) {
|
|
16
|
-
currentDump = {
|
|
17
|
-
groupName,
|
|
18
|
-
executions: [],
|
|
19
|
-
};
|
|
20
|
-
dumps.push(currentDump);
|
|
21
|
-
}
|
|
22
|
-
currentDump.executions.push(execution);
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const writeOutActionDumps = () => {
|
|
26
|
-
writeDumpFile(`playwright-${process.pid}`, groupedActionDumpFileExt, JSON.stringify(dumps));
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const groupAndCaseForTest = (testInfo: TestInfo) => {
|
|
30
|
-
let groupName: string;
|
|
31
|
-
let caseName: string;
|
|
32
|
-
const titlePath = [...testInfo.titlePath];
|
|
33
|
-
|
|
34
|
-
if (titlePath.length > 1) {
|
|
35
|
-
caseName = titlePath.pop()!;
|
|
36
|
-
groupName = titlePath.join(' > ');
|
|
37
|
-
} else if (titlePath.length === 1) {
|
|
38
|
-
caseName = titlePath[0];
|
|
39
|
-
groupName = caseName;
|
|
40
|
-
} else {
|
|
41
|
-
caseName = 'unnamed';
|
|
42
|
-
groupName = 'unnamed';
|
|
43
|
-
}
|
|
44
|
-
return { groupName, caseName };
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const aiAction = async (page: any, testInfo: TestInfo, taskPrompt: string) => {
|
|
48
|
-
const { groupName, caseName } = groupAndCaseForTest(testInfo);
|
|
49
|
-
|
|
50
|
-
const actionAgent = new PlayWrightActionAgent(page, { taskName: caseName });
|
|
51
|
-
let error: Error | undefined;
|
|
52
|
-
try {
|
|
53
|
-
await actionAgent.action(taskPrompt);
|
|
54
|
-
} catch (e: any) {
|
|
55
|
-
error = e;
|
|
56
|
-
}
|
|
57
|
-
if (actionAgent.actionDump) {
|
|
58
|
-
appendDump(groupName, actionAgent.actionDump);
|
|
59
|
-
writeOutActionDumps();
|
|
60
|
-
}
|
|
61
|
-
if (error) {
|
|
62
|
-
// playwright cli won't print error cause, so we print it here
|
|
63
|
-
console.error(error);
|
|
64
|
-
throw new Error(error.message, { cause: error });
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const aiQuery = async (page: any, testInfo: TestInfo, demand: any) => {
|
|
69
|
-
const { groupName, caseName } = groupAndCaseForTest(testInfo);
|
|
70
|
-
|
|
71
|
-
const actionAgent = new PlayWrightActionAgent(page, { taskName: caseName });
|
|
72
|
-
let error: Error | undefined;
|
|
73
|
-
let result: any;
|
|
74
|
-
try {
|
|
75
|
-
result = await actionAgent.query(demand);
|
|
76
|
-
} catch (e: any) {
|
|
77
|
-
error = e;
|
|
78
|
-
}
|
|
79
|
-
if (actionAgent.actionDump) {
|
|
80
|
-
appendDump(groupName, actionAgent.actionDump);
|
|
81
|
-
writeOutActionDumps();
|
|
82
|
-
}
|
|
83
|
-
if (error) {
|
|
84
|
-
// playwright cli won't print error cause, so we print it here
|
|
85
|
-
console.error(error);
|
|
86
|
-
throw new Error(error.message, { cause: error });
|
|
87
|
-
}
|
|
88
|
-
return result;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
// shortcut
|
|
93
|
-
ai: async ({ page }: any, use: any, testInfo: TestInfo) => {
|
|
94
|
-
await use(async (taskPrompt: string, type = 'action') => {
|
|
95
|
-
if (type === 'action') {
|
|
96
|
-
return aiAction(page, testInfo, taskPrompt);
|
|
97
|
-
} else if (type === 'query') {
|
|
98
|
-
return aiQuery(page, testInfo, taskPrompt);
|
|
99
|
-
}
|
|
100
|
-
throw new Error(`Unknown or Unsupported task type: ${type}, only support 'action' or 'query'`);
|
|
101
|
-
});
|
|
102
|
-
},
|
|
103
|
-
aiAction: async ({ page }: any, use: any, testInfo: TestInfo) => {
|
|
104
|
-
await use(async (taskPrompt: string) => {
|
|
105
|
-
await aiAction(page, testInfo, taskPrompt);
|
|
106
|
-
});
|
|
107
|
-
},
|
|
108
|
-
aiQuery: async ({ page }: any, use: any, testInfo: TestInfo) => {
|
|
109
|
-
await use(async function (demand: any) {
|
|
110
|
-
return aiQuery(page, testInfo, demand);
|
|
111
|
-
});
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
export type PlayWrightAiFixtureType = {
|
|
117
|
-
ai: <T = any>(prompt: string, type?: 'action' | 'query') => Promise<T>;
|
|
118
|
-
aiAction: (taskPrompt: string) => ReturnType<PlayWrightActionAgent['action']>;
|
|
119
|
-
aiQuery: <T = any>(demand: any) => Promise<T>;
|
|
120
|
-
};
|
package/src/playwright/utils.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import fs, { readFileSync } from 'fs';
|
|
2
|
-
import assert from 'assert';
|
|
3
|
-
import { Buffer } from 'buffer';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import type { Page as PlaywrightPage } from 'playwright';
|
|
6
|
-
import { Page } from 'puppeteer';
|
|
7
|
-
import { UIContext, PlaywrightParserOpt } from '@midscene/core';
|
|
8
|
-
import { alignCoordByTrim, base64Encoded, imageInfo, imageInfoOfBase64 } from '@midscene/core/image';
|
|
9
|
-
import { getTmpFile } from '@midscene/core/utils';
|
|
10
|
-
import { WebElementInfo, WebElementInfoType } from './element';
|
|
11
|
-
|
|
12
|
-
export async function parseContextFromPlaywrightPage(
|
|
13
|
-
page: PlaywrightPage,
|
|
14
|
-
_opt?: PlaywrightParserOpt,
|
|
15
|
-
): Promise<UIContext<WebElementInfo>> {
|
|
16
|
-
assert(page, 'page is required');
|
|
17
|
-
const file = '/Users/bytedance/workspace/midscene/packages/midscene/tests/fixtures/heytea.jpeg'; // getTmpFile('jpeg');
|
|
18
|
-
await page.screenshot({ path: file, type: 'jpeg', quality: 75 });
|
|
19
|
-
const screenshotBuffer = readFileSync(file);
|
|
20
|
-
const screenshotBase64 = base64Encoded(file);
|
|
21
|
-
const captureElementSnapshot = await getElementInfosFromPage(page);
|
|
22
|
-
// align element
|
|
23
|
-
const elementsInfo = await alignElements(screenshotBuffer, captureElementSnapshot, page);
|
|
24
|
-
const size = await imageInfoOfBase64(screenshotBase64);
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
content: elementsInfo,
|
|
28
|
-
size,
|
|
29
|
-
screenshotBase64,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export async function getElementInfosFromPage(page: Page | PlaywrightPage) {
|
|
34
|
-
const pathDir = findNearestPackageJson(__dirname);
|
|
35
|
-
assert(pathDir, `can't find pathDir, with ${__dirname}`);
|
|
36
|
-
const scriptPath = path.join(pathDir, './dist/script/htmlElement.js');
|
|
37
|
-
const elementInfosScriptContent = readFileSync(scriptPath, 'utf-8');
|
|
38
|
-
const extraReturnLogic = `${elementInfosScriptContent}midscene_element_inspector.extractTextWithPositionDFS()`;
|
|
39
|
-
|
|
40
|
-
const captureElementSnapshot = await (page as any).evaluate(extraReturnLogic);
|
|
41
|
-
return captureElementSnapshot;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function alignElements(
|
|
45
|
-
screenshotBuffer: Buffer,
|
|
46
|
-
elements: WebElementInfoType[],
|
|
47
|
-
page: PlaywrightPage,
|
|
48
|
-
): Promise<WebElementInfo[]> {
|
|
49
|
-
const textsAligned: WebElementInfo[] = [];
|
|
50
|
-
for (const item of elements) {
|
|
51
|
-
const { rect } = item;
|
|
52
|
-
const aligned = await alignCoordByTrim(screenshotBuffer, rect);
|
|
53
|
-
item.rect = aligned;
|
|
54
|
-
item.center = [
|
|
55
|
-
Math.round(aligned.left + aligned.width / 2),
|
|
56
|
-
Math.round(aligned.top + aligned.height / 2),
|
|
57
|
-
];
|
|
58
|
-
textsAligned.push(
|
|
59
|
-
new WebElementInfo({
|
|
60
|
-
...item,
|
|
61
|
-
page,
|
|
62
|
-
}),
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
return textsAligned;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Find the nearest package.json file recursively
|
|
70
|
-
* @param {string} dir - Home directory
|
|
71
|
-
* @returns {string|null} - The most recent package.json file path or null
|
|
72
|
-
*/
|
|
73
|
-
function findNearestPackageJson(dir: string) {
|
|
74
|
-
const packageJsonPath = path.join(dir, 'package.json');
|
|
75
|
-
|
|
76
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
77
|
-
return dir;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const parentDir = path.dirname(dir);
|
|
81
|
-
|
|
82
|
-
// Return null if the root directory has been reached
|
|
83
|
-
if (parentDir === dir) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return findNearestPackageJson(parentDir);
|
|
88
|
-
}
|
package/src/puppeteer/element.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// import { Page } from 'puppeteer';
|
|
2
|
-
// import { BaseElement, Rect } from '@/types';
|
|
3
|
-
|
|
4
|
-
// export class Element implements BaseElement {
|
|
5
|
-
// id: string;
|
|
6
|
-
|
|
7
|
-
// attributes: Record<string,string>;
|
|
8
|
-
|
|
9
|
-
// nodeType: string;
|
|
10
|
-
|
|
11
|
-
// content: string;
|
|
12
|
-
|
|
13
|
-
// locator: string;
|
|
14
|
-
|
|
15
|
-
// rect: Rect;
|
|
16
|
-
|
|
17
|
-
// center: [number, number];
|
|
18
|
-
|
|
19
|
-
// page: Page;
|
|
20
|
-
|
|
21
|
-
// constructor(options: {
|
|
22
|
-
// id: string, attributes: Record<string, string>, nodeType: string, content: string, rect: Rect, page: Page, locator: string
|
|
23
|
-
// }) {
|
|
24
|
-
// this.id = options.id;
|
|
25
|
-
// this.attributes = options.attributes;
|
|
26
|
-
// this.nodeType = options.nodeType;
|
|
27
|
-
// this.content = options.content;
|
|
28
|
-
// this.rect = options.rect;
|
|
29
|
-
// this.center = [Math.floor(options.rect.left + options.rect.width / 2), Math.floor(options.rect.top + options.rect.height / 2)];
|
|
30
|
-
// this.page = options.page;
|
|
31
|
-
// this.locator = options.locator;
|
|
32
|
-
// }
|
|
33
|
-
|
|
34
|
-
// async tap() {
|
|
35
|
-
// await this.page.mouse.click(this.center[0], this.center[1]);
|
|
36
|
-
// }
|
|
37
|
-
|
|
38
|
-
// async hover() {
|
|
39
|
-
// console.log('hover');
|
|
40
|
-
// }
|
|
41
|
-
|
|
42
|
-
// async type(text: string) {
|
|
43
|
-
// await this.page.keyboard.type(text, { delay: 100 });
|
|
44
|
-
// }
|
|
45
|
-
|
|
46
|
-
// async press(key: string) {
|
|
47
|
-
// await this.page.keyboard.press(key as any, { delay: 100 });
|
|
48
|
-
// }
|
|
49
|
-
// }
|