@midscene/web 0.0.1 → 0.1.0

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 CHANGED
@@ -19,24 +19,21 @@ var __spreadValues = (a, b) => {
19
19
  var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
20
 
21
21
  // src/playwright/index.ts
22
- import { groupedActionDumpFileExt, writeDumpFile } from "@midscene/core/utils";
22
+ import { randomUUID } from "crypto";
23
23
 
24
- // src/playwright/actions.ts
25
- import assert2 from "assert";
26
- import Insight, {
27
- Executor,
28
- plan
29
- } from "@midscene/core";
30
- import { commonScreenshotParam, getTmpFile, sleep } from "@midscene/core/utils";
31
- import { base64Encoded as base64Encoded2 } from "@midscene/core/image";
24
+ // src/playwright/cache.ts
25
+ import path2, { join } from "path";
26
+ import fs2 from "fs";
27
+ import { writeDumpFile, getDumpDirPath } from "@midscene/core/utils";
32
28
 
33
- // src/playwright/utils.ts
29
+ // src/common/utils.ts
34
30
  import fs, { readFileSync } from "fs";
35
31
  import assert from "assert";
36
32
  import path from "path";
37
33
  import { alignCoordByTrim, base64Encoded, imageInfoOfBase64 } from "@midscene/core/image";
34
+ import { getTmpFile } from "@midscene/core/utils";
38
35
 
39
- // src/playwright/element.ts
36
+ // src/web-element.ts
40
37
  var WebElementInfo = class {
41
38
  constructor({
42
39
  content,
@@ -54,24 +51,13 @@ var WebElementInfo = class {
54
51
  this.id = id;
55
52
  this.attributes = attributes;
56
53
  }
57
- async tap() {
58
- await this.page.mouse.click(this.center[0], this.center[1]);
59
- }
60
- async hover() {
61
- await this.page.mouse.move(this.center[0], this.center[1]);
62
- }
63
- async type(text) {
64
- await this.page.keyboard.type(text);
65
- }
66
- async press(key) {
67
- await this.page.keyboard.press(key);
68
- }
69
54
  };
70
55
 
71
- // src/playwright/utils.ts
72
- async function parseContextFromPlaywrightPage(page, _opt) {
56
+ // src/common/utils.ts
57
+ async function parseContextFromWebPage(page, _opt) {
73
58
  assert(page, "page is required");
74
- const file = "/Users/bytedance/workspace/midscene/packages/midscene/tests/fixtures/heytea.jpeg";
59
+ const url = page.url();
60
+ const file = getTmpFile("jpeg");
75
61
  await page.screenshot({ path: file, type: "jpeg", quality: 75 });
76
62
  const screenshotBuffer = readFileSync(file);
77
63
  const screenshotBase64 = base64Encoded(file);
@@ -81,7 +67,8 @@ async function parseContextFromPlaywrightPage(page, _opt) {
81
67
  return {
82
68
  content: elementsInfo,
83
69
  size,
84
- screenshotBase64
70
+ screenshotBase64,
71
+ url
85
72
  };
86
73
  }
87
74
  async function getElementInfosFromPage(page) {
@@ -123,17 +110,138 @@ function findNearestPackageJson(dir) {
123
110
  return findNearestPackageJson(parentDir);
124
111
  }
125
112
 
126
- // src/playwright/actions.ts
127
- var PlayWrightActionAgent = class {
128
- constructor(page, opt) {
113
+ // src/playwright/cache.ts
114
+ function writeTestCache(taskFile, taskTitle, taskCacheJson) {
115
+ const packageJson = getPkgInfo();
116
+ writeDumpFile({
117
+ fileName: `${taskFile}(${taskTitle})`,
118
+ fileExt: "json",
119
+ fileContent: JSON.stringify(
120
+ __spreadValues({
121
+ pkgName: packageJson.name,
122
+ pkgVersion: packageJson.version,
123
+ taskFile,
124
+ taskTitle
125
+ }, taskCacheJson),
126
+ null,
127
+ 2
128
+ ),
129
+ type: "cache"
130
+ });
131
+ }
132
+ function readTestCache(taskFile, taskTitle) {
133
+ const cacheFile = join(getDumpDirPath("cache"), `${taskFile}(${taskTitle}).json`);
134
+ const pkgInfo = getPkgInfo();
135
+ if (process.env.MIDSCENE_CACHE === "true" && fs2.existsSync(cacheFile)) {
136
+ try {
137
+ const data = fs2.readFileSync(cacheFile, "utf8");
138
+ const jsonData = JSON.parse(data);
139
+ if (jsonData.pkgName !== pkgInfo.name || jsonData.pkgVersion !== pkgInfo.version) {
140
+ return void 0;
141
+ }
142
+ return jsonData;
143
+ } catch (err) {
144
+ return void 0;
145
+ }
146
+ }
147
+ return void 0;
148
+ }
149
+ function getPkgInfo() {
150
+ const packageJsonDir = findNearestPackageJson(__dirname);
151
+ if (!packageJsonDir) {
152
+ console.error("Cannot find package.json directory: ", __dirname);
153
+ return {
154
+ name: "@midscene/web",
155
+ version: "0.0.0"
156
+ };
157
+ }
158
+ const packageJsonPath = path2.join(packageJsonDir, "package.json");
159
+ const data = fs2.readFileSync(packageJsonPath, "utf8");
160
+ const packageJson = JSON.parse(data);
161
+ return {
162
+ name: packageJson.name,
163
+ version: packageJson.version
164
+ };
165
+ }
166
+
167
+ // src/common/agent.ts
168
+ import { groupedActionDumpFileExt, writeDumpFile as writeDumpFile2 } from "@midscene/core/utils";
169
+
170
+ // src/common/tasks.ts
171
+ import assert2 from "assert";
172
+ import Insight, {
173
+ Executor,
174
+ plan
175
+ } from "@midscene/core";
176
+ import { commonScreenshotParam, getTmpFile as getTmpFile2, sleep } from "@midscene/core/utils";
177
+ import { base64Encoded as base64Encoded2 } from "@midscene/core/image";
178
+
179
+ // src/common/task-cache.ts
180
+ var TaskCache = class {
181
+ constructor(opts) {
182
+ this.cache = opts == null ? void 0 : opts.cache;
183
+ this.newCache = {
184
+ aiTasks: []
185
+ };
186
+ }
187
+ readCache(pageContext, type, userPrompt) {
188
+ var _a;
189
+ if (this.cache) {
190
+ const { aiTasks } = this.cache;
191
+ const index = aiTasks.findIndex((item) => item.prompt === userPrompt);
192
+ if (index === -1) {
193
+ return false;
194
+ }
195
+ const taskRes = aiTasks.splice(index, 1)[0];
196
+ if ((taskRes == null ? void 0 : taskRes.type) === "locate" && !((_a = taskRes.response) == null ? void 0 : _a.elements.every((element) => {
197
+ const findIndex = pageContext.content.findIndex(
198
+ (contentElement) => contentElement.id === element.id
199
+ );
200
+ if (findIndex === -1) {
201
+ return false;
202
+ }
203
+ return true;
204
+ }))) {
205
+ return false;
206
+ }
207
+ if (taskRes && taskRes.type === type && taskRes.prompt === userPrompt && this.pageContextEqual(taskRes.pageContext, pageContext)) {
208
+ return taskRes.response;
209
+ }
210
+ }
211
+ return false;
212
+ }
213
+ saveCache(cache) {
214
+ var _a;
215
+ if (cache) {
216
+ (_a = this.newCache) == null ? void 0 : _a.aiTasks.push(cache);
217
+ }
218
+ }
219
+ pageContextEqual(taskPageContext, pageContext) {
220
+ return taskPageContext.size.width === pageContext.size.width && taskPageContext.size.height === pageContext.size.height;
221
+ }
222
+ /**
223
+ * Generate task cache data.
224
+ * This method is mainly used to create or obtain some cached data for tasks, and it returns a new cache object.
225
+ * In the cache object, it may contain task-related information, states, or other necessary data.
226
+ * It is assumed that the `newCache` property already exists in the current class or object and is a data structure used to store task cache.
227
+ * @returns {Object} Returns a new cache object, which may include task cache data.
228
+ */
229
+ generateTaskCache() {
230
+ return this.newCache;
231
+ }
232
+ };
233
+
234
+ // src/common/tasks.ts
235
+ var PageTaskExecutor = class {
236
+ constructor(page, opts) {
129
237
  this.page = page;
130
238
  this.insight = new Insight(async () => {
131
- return await parseContextFromPlaywrightPage(page);
239
+ return await parseContextFromWebPage(page);
132
240
  });
133
- this.executor = new Executor((opt == null ? void 0 : opt.taskName) || "MidScene - PlayWrightAI");
241
+ this.taskCache = new TaskCache(opts);
134
242
  }
135
243
  async recordScreenshot(timing) {
136
- const file = getTmpFile("jpeg");
244
+ const file = getTmpFile2("jpeg");
137
245
  await this.page.screenshot(__spreadProps(__spreadValues({}, commonScreenshotParam), {
138
246
  path: file
139
247
  }));
@@ -179,14 +287,41 @@ var PlayWrightActionAgent = class {
179
287
  insightDump = dump;
180
288
  };
181
289
  this.insight.onceDumpUpdatedFn = dumpCollector;
182
- const element = await this.insight.locate(param.prompt);
290
+ const pageContext = await this.insight.contextRetrieverFn();
291
+ const locateCache = this.taskCache.readCache(pageContext, "locate", param.prompt);
292
+ let locateResult;
293
+ const callAI = this.insight.aiVendorFn;
294
+ const element = await this.insight.locate(param.prompt, {
295
+ callAI: async (message) => {
296
+ if (locateCache) {
297
+ locateResult = locateCache;
298
+ return Promise.resolve(locateCache);
299
+ }
300
+ locateResult = await callAI(message);
301
+ return locateResult;
302
+ }
303
+ });
183
304
  assert2(element, `Element not found: ${param.prompt}`);
305
+ if (locateResult) {
306
+ this.taskCache.saveCache({
307
+ type: "locate",
308
+ pageContext: {
309
+ url: pageContext.url,
310
+ size: pageContext.size
311
+ },
312
+ prompt: param.prompt,
313
+ response: locateResult
314
+ });
315
+ }
184
316
  return {
185
317
  output: {
186
318
  element
187
319
  },
188
320
  log: {
189
321
  dump: insightDump
322
+ },
323
+ cache: {
324
+ hit: Boolean(locateResult)
190
325
  }
191
326
  };
192
327
  }
@@ -272,43 +407,66 @@ var PlayWrightActionAgent = class {
272
407
  return tasks;
273
408
  }
274
409
  async action(userPrompt) {
275
- this.executor.description = userPrompt;
276
- const pageContext = await this.insight.contextRetrieverFn();
410
+ const taskExecutor = new Executor(userPrompt);
411
+ taskExecutor.description = userPrompt;
277
412
  let plans = [];
278
413
  const planningTask = {
279
414
  type: "Planning",
280
415
  param: {
281
416
  userPrompt
282
417
  },
283
- async executor(param) {
284
- const planResult = await plan(pageContext, param.userPrompt);
418
+ executor: async (param) => {
419
+ const pageContext = await this.insight.contextRetrieverFn();
420
+ let planResult;
421
+ const planCache = this.taskCache.readCache(pageContext, "plan", userPrompt);
422
+ if (planCache) {
423
+ planResult = planCache;
424
+ } else {
425
+ planResult = await plan(param.userPrompt, {
426
+ context: pageContext
427
+ });
428
+ }
285
429
  assert2(planResult.plans.length > 0, "No plans found");
286
430
  plans = planResult.plans;
431
+ this.taskCache.saveCache({
432
+ type: "plan",
433
+ pageContext: {
434
+ url: pageContext.url,
435
+ size: pageContext.size
436
+ },
437
+ prompt: userPrompt,
438
+ response: planResult
439
+ });
287
440
  return {
288
- output: planResult
441
+ output: planResult,
442
+ cache: {
443
+ hint: Boolean(planCache)
444
+ }
289
445
  };
290
446
  }
291
447
  };
292
448
  try {
293
- await this.executor.append(this.wrapExecutorWithScreenshot(planningTask));
294
- await this.executor.flush();
295
- this.actionDump = this.executor.dump();
449
+ await taskExecutor.append(this.wrapExecutorWithScreenshot(planningTask));
450
+ await taskExecutor.flush();
451
+ this.executionDump = taskExecutor.dump();
296
452
  const executables = await this.convertPlanToExecutable(plans);
297
- await this.executor.append(executables);
298
- await this.executor.flush();
299
- this.actionDump = this.executor.dump();
453
+ await taskExecutor.append(executables);
454
+ await taskExecutor.flush();
455
+ this.executionDump = taskExecutor.dump();
300
456
  assert2(
301
- this.executor.status !== "error",
302
- `failed to execute tasks: ${this.executor.status}, msg: ${this.executor.errorMsg || ""}`
457
+ taskExecutor.status !== "error",
458
+ `failed to execute tasks: ${taskExecutor.status}, msg: ${taskExecutor.errorMsg || ""}`
303
459
  );
304
460
  } catch (e) {
305
- this.actionDump = this.executor.dump();
461
+ this.executionDump = taskExecutor.dump();
306
462
  const err = new Error(e.message, { cause: e });
307
463
  throw err;
308
464
  }
309
465
  }
310
466
  async query(demand) {
311
- this.executor.description = JSON.stringify(demand);
467
+ const description = JSON.stringify(demand);
468
+ const taskExecutor = new Executor(description);
469
+ taskExecutor.description = description;
312
470
  let data;
313
471
  const queryTask = {
314
472
  type: "Insight",
@@ -330,11 +488,11 @@ var PlayWrightActionAgent = class {
330
488
  }
331
489
  };
332
490
  try {
333
- await this.executor.append(this.wrapExecutorWithScreenshot(queryTask));
334
- await this.executor.flush();
335
- this.actionDump = this.executor.dump();
491
+ await taskExecutor.append(this.wrapExecutorWithScreenshot(queryTask));
492
+ await taskExecutor.flush();
493
+ this.executionDump = taskExecutor.dump();
336
494
  } catch (e) {
337
- this.actionDump = this.executor.dump();
495
+ this.executionDump = taskExecutor.dump();
338
496
  const err = new Error(e.message, { cause: e });
339
497
  throw err;
340
498
  }
@@ -342,101 +500,170 @@ var PlayWrightActionAgent = class {
342
500
  }
343
501
  };
344
502
 
345
- // src/playwright/index.ts
346
- var PlaywrightAiFixture = () => {
347
- const dumps = [];
348
- const appendDump = (groupName, execution) => {
349
- let currentDump = dumps.find((dump) => dump.groupName === groupName);
350
- if (!currentDump) {
351
- currentDump = {
352
- groupName,
503
+ // src/common/agent.ts
504
+ var PageAgent = class {
505
+ constructor(page, opts) {
506
+ this.page = page;
507
+ this.dumps = [
508
+ {
509
+ groupName: (opts == null ? void 0 : opts.taskFile) || "unnamed",
353
510
  executions: []
354
- };
355
- dumps.push(currentDump);
356
- }
511
+ }
512
+ ];
513
+ this.testId = (opts == null ? void 0 : opts.testId) || String(process.pid);
514
+ this.actionAgent = new PageTaskExecutor(this.page, {
515
+ cache: (opts == null ? void 0 : opts.cache) || { aiTasks: [] }
516
+ });
517
+ }
518
+ appendDump(execution) {
519
+ const currentDump = this.dumps[0];
357
520
  currentDump.executions.push(execution);
358
- };
359
- const writeOutActionDumps = () => {
360
- writeDumpFile(`playwright-${process.pid}`, groupedActionDumpFileExt, JSON.stringify(dumps));
361
- };
362
- const groupAndCaseForTest = (testInfo) => {
363
- let groupName;
364
- let caseName;
365
- const titlePath = [...testInfo.titlePath];
366
- if (titlePath.length > 1) {
367
- caseName = titlePath.pop();
368
- groupName = titlePath.join(" > ");
369
- } else if (titlePath.length === 1) {
370
- caseName = titlePath[0];
371
- groupName = caseName;
372
- } else {
373
- caseName = "unnamed";
374
- groupName = "unnamed";
375
- }
376
- return { groupName, caseName };
377
- };
378
- const aiAction = async (page, testInfo, taskPrompt) => {
379
- const { groupName, caseName } = groupAndCaseForTest(testInfo);
380
- const actionAgent = new PlayWrightActionAgent(page, { taskName: caseName });
521
+ }
522
+ writeOutActionDumps() {
523
+ this.dumpFile = writeDumpFile2({
524
+ fileName: `playwright-${this.testId}`,
525
+ fileExt: groupedActionDumpFileExt,
526
+ fileContent: JSON.stringify(this.dumps)
527
+ });
528
+ }
529
+ async aiAction(taskPrompt) {
381
530
  let error;
382
531
  try {
383
- await actionAgent.action(taskPrompt);
532
+ await this.actionAgent.action(taskPrompt);
384
533
  } catch (e) {
385
534
  error = e;
386
535
  }
387
- if (actionAgent.actionDump) {
388
- appendDump(groupName, actionAgent.actionDump);
389
- writeOutActionDumps();
536
+ if (this.actionAgent.executionDump) {
537
+ this.appendDump(this.actionAgent.executionDump);
538
+ this.writeOutActionDumps();
390
539
  }
391
540
  if (error) {
392
541
  console.error(error);
393
542
  throw new Error(error.message, { cause: error });
394
543
  }
395
- };
396
- const aiQuery = async (page, testInfo, demand) => {
397
- const { groupName, caseName } = groupAndCaseForTest(testInfo);
398
- const actionAgent = new PlayWrightActionAgent(page, { taskName: caseName });
544
+ }
545
+ async aiQuery(demand) {
399
546
  let error;
400
547
  let result;
401
548
  try {
402
- result = await actionAgent.query(demand);
549
+ result = await this.actionAgent.query(demand);
403
550
  } catch (e) {
404
551
  error = e;
405
552
  }
406
- if (actionAgent.actionDump) {
407
- appendDump(groupName, actionAgent.actionDump);
408
- writeOutActionDumps();
553
+ if (this.actionAgent.executionDump) {
554
+ this.appendDump(this.actionAgent.executionDump);
555
+ this.writeOutActionDumps();
409
556
  }
410
557
  if (error) {
411
558
  console.error(error);
412
559
  throw new Error(error.message, { cause: error });
413
560
  }
414
561
  return result;
562
+ }
563
+ async ai(taskPrompt, type = "action") {
564
+ if (type === "action") {
565
+ return this.aiAction(taskPrompt);
566
+ } else if (type === "query") {
567
+ return this.aiQuery(taskPrompt);
568
+ }
569
+ throw new Error(`Unknown or Unsupported task type: ${type}, only support 'action' or 'query'`);
570
+ }
571
+ };
572
+
573
+ // src/playwright/index.ts
574
+ var groupAndCaseForTest = (testInfo) => {
575
+ let taskFile;
576
+ let taskTitle;
577
+ const titlePath = [...testInfo.titlePath];
578
+ if (titlePath.length > 1) {
579
+ taskTitle = titlePath.pop();
580
+ taskFile = `${titlePath.join(" > ")}:${testInfo.line}`;
581
+ } else if (titlePath.length === 1) {
582
+ taskTitle = titlePath[0];
583
+ taskFile = `${taskTitle}:${testInfo.line}`;
584
+ } else {
585
+ taskTitle = "unnamed";
586
+ taskFile = "unnamed";
587
+ }
588
+ return { taskFile, taskTitle };
589
+ };
590
+ var midSceneAgentKeyId = "_midSceneAgentId";
591
+ var PlaywrightAiFixture = () => {
592
+ const pageAgentMap = {};
593
+ const agentForPage = (page, opts) => {
594
+ let idForPage = page[midSceneAgentKeyId];
595
+ if (!idForPage) {
596
+ idForPage = randomUUID();
597
+ page[midSceneAgentKeyId] = idForPage;
598
+ const testCase = readTestCache(opts.taskFile, opts.taskTitle) || { aiTasks: [] };
599
+ pageAgentMap[idForPage] = new PageAgent(page, {
600
+ testId: `${opts.testId}-${idForPage}`,
601
+ taskFile: opts.taskFile,
602
+ cache: testCase
603
+ });
604
+ }
605
+ return pageAgentMap[idForPage];
415
606
  };
416
607
  return {
417
- // shortcut
418
608
  ai: async ({ page }, use, testInfo) => {
419
- await use(async (taskPrompt, type = "action") => {
420
- if (type === "action") {
421
- return aiAction(page, testInfo, taskPrompt);
422
- } else if (type === "query") {
423
- return aiQuery(page, testInfo, taskPrompt);
424
- }
425
- throw new Error(`Unknown or Unsupported task type: ${type}, only support 'action' or 'query'`);
609
+ const { taskFile, taskTitle } = groupAndCaseForTest(testInfo);
610
+ const agent = agentForPage(page, { testId: testInfo.testId, taskFile, taskTitle });
611
+ await use(async (taskPrompt, opts) => {
612
+ await page.waitForLoadState("networkidle");
613
+ const actionType = (opts == null ? void 0 : opts.type) || "action";
614
+ const result = await agent.ai(taskPrompt, actionType);
615
+ return result;
426
616
  });
617
+ const taskCacheJson = agent.actionAgent.taskCache.generateTaskCache();
618
+ writeTestCache(taskFile, taskTitle, taskCacheJson);
619
+ if (agent.dumpFile) {
620
+ testInfo.annotations.push({
621
+ type: "MIDSCENE_AI_ACTION",
622
+ description: JSON.stringify({
623
+ testId: testInfo.testId,
624
+ dumpPath: agent.dumpFile
625
+ })
626
+ });
627
+ }
427
628
  },
428
629
  aiAction: async ({ page }, use, testInfo) => {
630
+ const { taskFile, taskTitle } = groupAndCaseForTest(testInfo);
631
+ const agent = agentForPage(page, { testId: testInfo.testId, taskFile, taskTitle });
429
632
  await use(async (taskPrompt) => {
430
- await aiAction(page, testInfo, taskPrompt);
633
+ await page.waitForLoadState("networkidle");
634
+ await agent.aiAction(taskPrompt);
431
635
  });
636
+ if (agent.dumpFile) {
637
+ testInfo.annotations.push({
638
+ type: "MIDSCENE_AI_ACTION",
639
+ description: JSON.stringify({
640
+ testId: testInfo.testId,
641
+ dumpPath: agent.dumpFile
642
+ })
643
+ });
644
+ }
432
645
  },
433
646
  aiQuery: async ({ page }, use, testInfo) => {
647
+ const { taskFile, taskTitle } = groupAndCaseForTest(testInfo);
648
+ const agent = agentForPage(page, { testId: testInfo.testId, taskFile, taskTitle });
434
649
  await use(async function(demand) {
435
- return aiQuery(page, testInfo, demand);
650
+ await page.waitForLoadState("networkidle");
651
+ const result = await agent.aiQuery(demand);
652
+ return result;
436
653
  });
654
+ if (agent.dumpFile) {
655
+ testInfo.annotations.push({
656
+ type: "MIDSCENE_AI_ACTION",
657
+ description: JSON.stringify({
658
+ testId: testInfo.testId,
659
+ dumpPath: agent.dumpFile
660
+ })
661
+ });
662
+ }
437
663
  }
438
664
  };
439
665
  };
440
666
  export {
441
- PlaywrightAiFixture
667
+ PlaywrightAiFixture,
668
+ PageAgent as PuppeteerAgent
442
669
  };