@midscene/web 0.3.0 → 0.3.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021-present Midscene.js
3
+ Copyright (c) 2024-present Bytedance, Inc. and its affiliates.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/dist/es/index.js CHANGED
@@ -21,7 +21,7 @@ var __spreadValues = (a, b) => {
21
21
  return a;
22
22
  };
23
23
  var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
24
- var __commonJS = (cb, mod) => function __require() {
24
+ var __commonJS = (cb, mod) => function __require2() {
25
25
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
26
26
  };
27
27
  var __copyProps = (to, from, except, desc) => {
@@ -416,7 +416,6 @@ import assert from "assert";
416
416
  import fs, { readFileSync } from "fs";
417
417
  import path from "path";
418
418
  import {
419
- alignCoordByTrim,
420
419
  base64Encoded,
421
420
  imageInfoOfBase64
422
421
  } from "@midscene/core/image";
@@ -483,17 +482,16 @@ async function alignElements(screenshotBuffer, elements, page) {
483
482
  return item.rect.height >= sizeThreshold && item.rect.width >= sizeThreshold;
484
483
  });
485
484
  for (const item of validElements) {
486
- const { rect } = item;
487
- const aligned = await alignCoordByTrim(screenshotBuffer, rect);
488
- item.rect = aligned;
489
- item.center = [
490
- Math.round(aligned.left + aligned.width / 2),
491
- Math.round(aligned.top + aligned.height / 2)
492
- ];
485
+ const { rect, id, content, attributes, locator } = item;
493
486
  textsAligned.push(
494
- new WebElementInfo(__spreadProps(__spreadValues({}, item), {
487
+ new WebElementInfo({
488
+ rect,
489
+ locator,
490
+ id,
491
+ content,
492
+ attributes,
495
493
  page
496
- }))
494
+ })
497
495
  );
498
496
  }
499
497
  return textsAligned;
@@ -617,7 +615,7 @@ var PageTaskExecutor = class {
617
615
  };
618
616
  return taskFind;
619
617
  }
620
- if (plan2.type === "Assert") {
618
+ if (plan2.type === "Assert" || plan2.type === "AssertWithoutThrow") {
621
619
  const assertPlan = plan2;
622
620
  const taskAssert = {
623
621
  type: "Insight",
@@ -634,13 +632,16 @@ var PageTaskExecutor = class {
634
632
  assertPlan.param.assertion
635
633
  );
636
634
  if (!assertion.pass) {
637
- task.output = assertion;
638
- task.log = {
639
- dump: insightDump
640
- };
641
- throw new Error(
642
- assertion.thought || "Assertion failed without reason"
643
- );
635
+ if (plan2.type === "Assert") {
636
+ task.output = assertion;
637
+ task.log = {
638
+ dump: insightDump
639
+ };
640
+ throw new Error(
641
+ assertion.thought || "Assertion failed without reason"
642
+ );
643
+ }
644
+ task.error = assertion.thought;
644
645
  }
645
646
  return {
646
647
  output: assertion,
@@ -756,7 +757,19 @@ var PageTaskExecutor = class {
756
757
  return taskActionSleep;
757
758
  }
758
759
  if (plan2.type === "Error") {
759
- throw new Error(`Got a task plan with type Error: ${plan2.thought}`);
760
+ const taskActionError = {
761
+ type: "Action",
762
+ subType: "Error",
763
+ param: plan2.param,
764
+ executor: async (taskParam) => {
765
+ assert2(
766
+ taskParam.thought,
767
+ "An error occurred, but no thought provided"
768
+ );
769
+ throw new Error(taskParam.thought);
770
+ }
771
+ };
772
+ return taskActionError;
760
773
  }
761
774
  throw new Error(`Unknown or Unsupported task type: ${plan2.type}`);
762
775
  }).map((task) => {
@@ -766,7 +779,6 @@ var PageTaskExecutor = class {
766
779
  }
767
780
  async action(userPrompt) {
768
781
  const taskExecutor = new Executor(userPrompt);
769
- taskExecutor.description = userPrompt;
770
782
  let plans = [];
771
783
  const planningTask = {
772
784
  type: "Planning",
@@ -826,7 +838,6 @@ var PageTaskExecutor = class {
826
838
  async query(demand) {
827
839
  const description = typeof demand === "string" ? demand : JSON.stringify(demand);
828
840
  const taskExecutor = new Executor(description);
829
- taskExecutor.description = description;
830
841
  const queryTask = {
831
842
  type: "Insight",
832
843
  subType: "Query",
@@ -854,9 +865,8 @@ var PageTaskExecutor = class {
854
865
  };
855
866
  }
856
867
  async assert(assertion) {
857
- const description = assertion;
868
+ const description = `assert: ${assertion}`;
858
869
  const taskExecutor = new Executor(description);
859
- taskExecutor.description = description;
860
870
  const assertionPlan = {
861
871
  type: "Assert",
862
872
  param: {
@@ -871,6 +881,64 @@ var PageTaskExecutor = class {
871
881
  executor: taskExecutor
872
882
  };
873
883
  }
884
+ async waitFor(assertion, opt) {
885
+ const description = `waitFor: ${assertion}`;
886
+ const taskExecutor = new Executor(description);
887
+ const { timeoutMs, checkIntervalMs } = opt;
888
+ assert2(assertion, "No assertion for waitFor");
889
+ assert2(timeoutMs, "No timeoutMs for waitFor");
890
+ assert2(checkIntervalMs, "No checkIntervalMs for waitFor");
891
+ const overallStartTime = Date.now();
892
+ let startTime = Date.now();
893
+ let errorThought = "";
894
+ while (Date.now() - overallStartTime < timeoutMs) {
895
+ startTime = Date.now();
896
+ const assertPlan = {
897
+ type: "AssertWithoutThrow",
898
+ param: {
899
+ assertion
900
+ }
901
+ };
902
+ const assertTask = await this.convertPlanToExecutable([assertPlan]);
903
+ await taskExecutor.append(this.wrapExecutorWithScreenshot(assertTask[0]));
904
+ const output = await taskExecutor.flush();
905
+ if (output.pass) {
906
+ return {
907
+ output: void 0,
908
+ executor: taskExecutor
909
+ };
910
+ }
911
+ errorThought = output.thought;
912
+ const now = Date.now();
913
+ if (now - startTime < checkIntervalMs) {
914
+ const timeRemaining = checkIntervalMs - (now - startTime);
915
+ const sleepPlan = {
916
+ type: "Sleep",
917
+ param: {
918
+ timeMs: timeRemaining
919
+ }
920
+ };
921
+ const sleepTask = await this.convertPlanToExecutable([sleepPlan]);
922
+ await taskExecutor.append(
923
+ this.wrapExecutorWithScreenshot(sleepTask[0])
924
+ );
925
+ await taskExecutor.flush();
926
+ }
927
+ }
928
+ const errorPlan = {
929
+ type: "Error",
930
+ param: {
931
+ thought: `waitFor timeout: ${errorThought}`
932
+ }
933
+ };
934
+ const errorTask = await this.convertPlanToExecutable([errorPlan]);
935
+ await taskExecutor.append(errorTask[0]);
936
+ await taskExecutor.flush();
937
+ return {
938
+ output: void 0,
939
+ executor: taskExecutor
940
+ };
941
+ }
874
942
  };
875
943
 
876
944
  // src/common/agent.ts
@@ -880,6 +948,7 @@ var PageAgent = class {
880
948
  this.opts = Object.assign(
881
949
  {
882
950
  generateReport: true,
951
+ autoPrintReportMsg: true,
883
952
  groupName: "Midscene Report",
884
953
  groupDescription: ""
885
954
  },
@@ -905,7 +974,7 @@ var PageAgent = class {
905
974
  return stringifyDumpData(this.dump);
906
975
  }
907
976
  writeOutActionDumps() {
908
- const generateReport = this.opts.generateReport;
977
+ const { generateReport, autoPrintReportMsg } = this.opts;
909
978
  this.reportFile = writeLogFile({
910
979
  fileName: this.reportFileName,
911
980
  fileExt: groupedActionDumpFileExt,
@@ -913,7 +982,7 @@ var PageAgent = class {
913
982
  type: "dump",
914
983
  generateReport
915
984
  });
916
- if (generateReport) {
985
+ if (generateReport && autoPrintReportMsg) {
917
986
  printReportMsg(this.reportFile);
918
987
  }
919
988
  }
@@ -949,6 +1018,20 @@ ${errorTask == null ? void 0 : errorTask.errorStack}`);
949
1018
  ${reasonMsg}`);
950
1019
  }
951
1020
  }
1021
+ async aiWaitFor(assertion, opt) {
1022
+ const { executor } = await this.taskExecutor.waitFor(assertion, {
1023
+ timeoutMs: (opt == null ? void 0 : opt.timeoutMs) || 15 * 1e3,
1024
+ checkIntervalMs: (opt == null ? void 0 : opt.checkIntervalMs) || 3 * 1e3,
1025
+ assertion
1026
+ });
1027
+ this.appendExecutionDump(executor.dump());
1028
+ this.writeOutActionDumps();
1029
+ if (executor.isInErrorState()) {
1030
+ const errorTask = executor.latestErrorTask();
1031
+ throw new Error(`${errorTask == null ? void 0 : errorTask.error}
1032
+ ${errorTask == null ? void 0 : errorTask.errorStack}`);
1033
+ }
1034
+ }
952
1035
  async ai(taskPrompt, type = "action") {
953
1036
  if (type === "action") {
954
1037
  return this.aiAction(taskPrompt);
@@ -965,6 +1048,9 @@ ${reasonMsg}`);
965
1048
  }
966
1049
  };
967
1050
 
1051
+ // src/playwright/index.ts
1052
+ import { test } from "@playwright/test";
1053
+
968
1054
  // src/playwright/cache.ts
969
1055
  import fs2 from "fs";
970
1056
  import path2, { join } from "path";
@@ -1070,14 +1156,14 @@ var PlaywrightAiFixture = () => {
1070
1156
  }
1071
1157
  return pageAgentMap[idForPage];
1072
1158
  };
1073
- const updateDumpAnnotation = (test, dump) => {
1074
- const currentAnnotation = test.annotations.find((item) => {
1159
+ const updateDumpAnnotation = (test2, dump) => {
1160
+ const currentAnnotation = test2.annotations.find((item) => {
1075
1161
  return item.type === midsceneDumpAnnotationId;
1076
1162
  });
1077
1163
  if (currentAnnotation) {
1078
1164
  currentAnnotation.description = dump;
1079
1165
  } else {
1080
- test.annotations.push({
1166
+ test2.annotations.push({
1081
1167
  type: midsceneDumpAnnotationId,
1082
1168
  description: dump
1083
1169
  });
@@ -1089,10 +1175,14 @@ var PlaywrightAiFixture = () => {
1089
1175
  const agent = agentForPage(page, testInfo);
1090
1176
  await use(
1091
1177
  async (taskPrompt, opts) => {
1092
- await page.waitForLoadState("networkidle");
1093
- const actionType = (opts == null ? void 0 : opts.type) || "action";
1094
- const result = await agent.ai(taskPrompt, actionType);
1095
- return result;
1178
+ return new Promise((resolve, reject) => {
1179
+ test.step(`ai - ${taskPrompt}`, async () => {
1180
+ await page.waitForLoadState("networkidle");
1181
+ const actionType = (opts == null ? void 0 : opts.type) || "action";
1182
+ const result = await agent.ai(taskPrompt, actionType);
1183
+ resolve(result);
1184
+ });
1185
+ });
1096
1186
  }
1097
1187
  );
1098
1188
  const taskCacheJson = agent.taskExecutor.taskCache.generateTaskCache();
@@ -1103,31 +1193,243 @@ var PlaywrightAiFixture = () => {
1103
1193
  const { taskFile, taskTitle } = groupAndCaseForTest(testInfo);
1104
1194
  const agent = agentForPage(page, testInfo);
1105
1195
  await use(async (taskPrompt) => {
1106
- await page.waitForLoadState("networkidle");
1107
- await agent.aiAction(taskPrompt);
1196
+ test.step(`aiAction - ${taskPrompt}`, async () => {
1197
+ await page.waitForLoadState("networkidle");
1198
+ await agent.aiAction(taskPrompt);
1199
+ });
1108
1200
  });
1109
1201
  updateDumpAnnotation(testInfo, agent.dumpDataString());
1110
1202
  },
1111
1203
  aiQuery: async ({ page }, use, testInfo) => {
1112
1204
  const agent = agentForPage(page, testInfo);
1113
1205
  await use(async (demand) => {
1114
- await page.waitForLoadState("networkidle");
1115
- const result = await agent.aiQuery(demand);
1116
- return result;
1206
+ return new Promise((resolve, reject) => {
1207
+ test.step(`aiQuery - ${JSON.stringify(demand)}`, async () => {
1208
+ await page.waitForLoadState("networkidle");
1209
+ const result = await agent.aiQuery(demand);
1210
+ resolve(result);
1211
+ });
1212
+ });
1117
1213
  });
1118
1214
  updateDumpAnnotation(testInfo, agent.dumpDataString());
1119
1215
  },
1120
1216
  aiAssert: async ({ page }, use, testInfo) => {
1121
1217
  const agent = agentForPage(page, testInfo);
1122
1218
  await use(async (assertion, errorMsg) => {
1123
- await page.waitForLoadState("networkidle");
1124
- await agent.aiAssert(assertion, errorMsg);
1219
+ return new Promise((resolve, reject) => {
1220
+ test.step(`aiAssert - ${assertion}`, async () => {
1221
+ await page.waitForLoadState("networkidle");
1222
+ await agent.aiAssert(assertion, errorMsg);
1223
+ resolve(null);
1224
+ });
1225
+ });
1226
+ });
1227
+ updateDumpAnnotation(testInfo, agent.dumpDataString());
1228
+ },
1229
+ aiWaitFor: async ({ page }, use, testInfo) => {
1230
+ const agent = agentForPage(page, testInfo);
1231
+ await use(async (assertion, opt) => {
1232
+ return new Promise((resolve, reject) => {
1233
+ test.step(`aiWaitFor - ${assertion}`, async () => {
1234
+ await agent.aiWaitFor(assertion, opt);
1235
+ resolve(null);
1236
+ });
1237
+ });
1125
1238
  });
1126
1239
  updateDumpAnnotation(testInfo, agent.dumpDataString());
1127
1240
  }
1128
1241
  };
1129
1242
  };
1243
+
1244
+ // src/debug/index.ts
1245
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
1246
+ import path3 from "path";
1247
+
1248
+ // src/img/img.ts
1249
+ import assert3 from "assert";
1250
+ import { Buffer as Buffer2 } from "buffer";
1251
+ import sharp from "sharp";
1252
+ var createSvgOverlay = (elements, imageWidth, imageHeight) => {
1253
+ let svgContent = `<svg width="${imageWidth}" height="${imageHeight}" xmlns="http://www.w3.org/2000/svg">`;
1254
+ const colors = [
1255
+ { rect: "blue", text: "white" },
1256
+ { rect: "green", text: "white" }
1257
+ ];
1258
+ svgContent += "<defs>";
1259
+ elements.forEach((element, index) => {
1260
+ svgContent += `
1261
+ <clipPath id="clip${index}">
1262
+ <rect x="${element.x}" y="${element.y}" width="${element.width}" height="${element.height}" />
1263
+ </clipPath>
1264
+ `;
1265
+ });
1266
+ svgContent += "</defs>";
1267
+ elements.forEach((element, index) => {
1268
+ const textWidth = element.label.length * 8;
1269
+ const textHeight = 12;
1270
+ const rectWidth = textWidth + 5;
1271
+ const rectHeight = textHeight + 4;
1272
+ let rectX = element.x - rectWidth;
1273
+ let rectY = element.y + element.height / 2 - textHeight / 2 - 2;
1274
+ let textX = rectX + rectWidth / 2;
1275
+ let textY = rectY + rectHeight / 2 + 6;
1276
+ if (rectX < 0) {
1277
+ rectX = element.x;
1278
+ rectY = element.y - rectHeight;
1279
+ textX = rectX + rectWidth / 2;
1280
+ textY = rectY + rectHeight / 2 + 6;
1281
+ }
1282
+ const color = colors[index % colors.length];
1283
+ svgContent += `
1284
+ <rect x="${element.x}" y="${element.y}" width="${element.width}" height="${element.height}"
1285
+ style="fill:none;stroke:${color.rect};stroke-width:4" clip-path="url(#clip${index})" />
1286
+ <rect x="${rectX}" y="${rectY}" width="${rectWidth}" height="${rectHeight}" style="fill:${color.rect};" />
1287
+ <text x="${textX}" y="${textY}"
1288
+ text-anchor="middle" dominant-baseline="middle" style="fill:${color.text};font-size:12px;font-weight:bold;">
1289
+ ${element.label}
1290
+ </text>
1291
+ `;
1292
+ });
1293
+ svgContent += "</svg>";
1294
+ return Buffer2.from(svgContent);
1295
+ };
1296
+ var processImageElementInfo = async (options) => {
1297
+ const base64Image = options.inputImgBase64.split(";base64,").pop();
1298
+ assert3(base64Image, "base64Image is undefined");
1299
+ const imageBuffer = Buffer2.from(base64Image, "base64");
1300
+ const metadata = await sharp(imageBuffer).metadata();
1301
+ const { width, height } = metadata;
1302
+ if (width && height) {
1303
+ const svgOverlay = createSvgOverlay(
1304
+ options.elementsPositionInfo,
1305
+ width,
1306
+ height
1307
+ );
1308
+ const svgOverlayWithoutText = createSvgOverlay(
1309
+ options.elementsPositionInfoWithoutText,
1310
+ width,
1311
+ height
1312
+ );
1313
+ const compositeElementInfoImgBase64 = await sharp(imageBuffer).composite([{ input: svgOverlay, blend: "over" }]).toBuffer().then((data) => {
1314
+ return data.toString("base64");
1315
+ }).catch((err) => {
1316
+ throw err;
1317
+ });
1318
+ const compositeElementInfoImgWithoutTextBase64 = await sharp(imageBuffer).composite([{ input: svgOverlayWithoutText, blend: "over" }]).toBuffer().then((data) => {
1319
+ return data.toString("base64");
1320
+ }).catch((err) => {
1321
+ throw err;
1322
+ });
1323
+ return {
1324
+ compositeElementInfoImgBase64,
1325
+ compositeElementInfoImgWithoutTextBase64
1326
+ };
1327
+ }
1328
+ throw Error("Image processing failed because width or height is undefined");
1329
+ };
1330
+
1331
+ // src/img/util.ts
1332
+ async function getElementInfos(page) {
1333
+ const captureElementSnapshot = await getElementInfosFromPage(page);
1334
+ const elementsPositionInfo = captureElementSnapshot.map((elementInfo) => {
1335
+ return {
1336
+ label: elementInfo.indexId.toString(),
1337
+ x: elementInfo.rect.left,
1338
+ y: elementInfo.rect.top,
1339
+ width: elementInfo.rect.width,
1340
+ height: elementInfo.rect.height,
1341
+ attributes: elementInfo.attributes
1342
+ };
1343
+ });
1344
+ const elementsPositionInfoWithoutText = elementsPositionInfo.filter(
1345
+ (elementInfo) => {
1346
+ if (elementInfo.attributes.nodeType === "TEXT Node" /* TEXT */) {
1347
+ return false;
1348
+ }
1349
+ return true;
1350
+ }
1351
+ );
1352
+ return {
1353
+ elementsPositionInfo,
1354
+ captureElementSnapshot,
1355
+ elementsPositionInfoWithoutText
1356
+ };
1357
+ }
1358
+
1359
+ // src/debug/index.ts
1360
+ import { resizeImg, saveBase64Image } from "@midscene/core/image";
1361
+ async function generateExtractData(page, targetDir, saveImgType) {
1362
+ const buffer = await page.screenshot({
1363
+ encoding: "base64"
1364
+ });
1365
+ const inputImgBase64 = buffer.toString("base64");
1366
+ const {
1367
+ elementsPositionInfo,
1368
+ captureElementSnapshot,
1369
+ elementsPositionInfoWithoutText
1370
+ } = await getElementInfos(page);
1371
+ const inputImagePath = path3.join(targetDir, "input.png");
1372
+ const outputImagePath = path3.join(targetDir, "output.png");
1373
+ const outputWithoutTextImgPath = path3.join(
1374
+ targetDir,
1375
+ "output_without_text.png"
1376
+ );
1377
+ const resizeOutputImgPath = path3.join(targetDir, "resize-output.png");
1378
+ const snapshotJsonPath = path3.join(targetDir, "element-snapshot.json");
1379
+ const {
1380
+ compositeElementInfoImgBase64,
1381
+ compositeElementInfoImgWithoutTextBase64
1382
+ } = await processImageElementInfo({
1383
+ elementsPositionInfo,
1384
+ elementsPositionInfoWithoutText,
1385
+ inputImgBase64
1386
+ });
1387
+ const resizeImgBase64 = await resizeImg(inputImgBase64);
1388
+ if (!(saveImgType == null ? void 0 : saveImgType.disableSnapshot)) {
1389
+ writeFileSyncWithDir(
1390
+ snapshotJsonPath,
1391
+ JSON.stringify(captureElementSnapshot, null, 2)
1392
+ );
1393
+ }
1394
+ if (!(saveImgType == null ? void 0 : saveImgType.disableInputImage)) {
1395
+ await saveBase64Image({
1396
+ base64Data: inputImgBase64,
1397
+ outputPath: inputImagePath
1398
+ });
1399
+ }
1400
+ if (!(saveImgType == null ? void 0 : saveImgType.disableOutputImage)) {
1401
+ await saveBase64Image({
1402
+ base64Data: compositeElementInfoImgBase64,
1403
+ outputPath: outputImagePath
1404
+ });
1405
+ }
1406
+ if (!(saveImgType == null ? void 0 : saveImgType.disableOutputWithoutTextImg)) {
1407
+ await saveBase64Image({
1408
+ base64Data: compositeElementInfoImgWithoutTextBase64,
1409
+ outputPath: outputWithoutTextImgPath
1410
+ });
1411
+ }
1412
+ if (!(saveImgType == null ? void 0 : saveImgType.disableResizeOutputImg)) {
1413
+ await saveBase64Image({
1414
+ base64Data: resizeImgBase64,
1415
+ outputPath: resizeOutputImgPath
1416
+ });
1417
+ }
1418
+ }
1419
+ function ensureDirectoryExistence(filePath) {
1420
+ const dirname2 = path3.dirname(filePath);
1421
+ if (existsSync(dirname2)) {
1422
+ return;
1423
+ }
1424
+ ensureDirectoryExistence(dirname2);
1425
+ mkdirSync(dirname2);
1426
+ }
1427
+ function writeFileSyncWithDir(filePath, content, options = {}) {
1428
+ ensureDirectoryExistence(filePath);
1429
+ writeFileSync(filePath, content, options);
1430
+ }
1130
1431
  export {
1131
1432
  PlaywrightAiFixture,
1132
- PageAgent as PuppeteerAgent
1433
+ PageAgent as PuppeteerAgent,
1434
+ generateExtractData
1133
1435
  };
@@ -325,7 +325,6 @@ import assert from "assert";
325
325
  import fs, { readFileSync } from "fs";
326
326
  import path from "path";
327
327
  import {
328
- alignCoordByTrim,
329
328
  base64Encoded,
330
329
  imageInfoOfBase64
331
330
  } from "@midscene/core/image";