2024-11-19 16:01:13 +08:00
|
|
|
import { execSync } from 'node:child_process';
|
2024-09-05 20:05:19 +08:00
|
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
2024-08-04 08:28:19 +08:00
|
|
|
import { tmpdir } from 'node:os';
|
2025-03-07 17:20:18 +08:00
|
|
|
import * as path from 'node:path';
|
|
|
|
|
import { dirname } from 'node:path';
|
2024-10-28 11:04:40 +08:00
|
|
|
import { getRunningPkgInfo } from '@midscene/shared/fs';
|
2025-03-24 09:50:27 +08:00
|
|
|
import { assert, getGlobalScope } from '@midscene/shared/utils';
|
2024-10-28 11:04:40 +08:00
|
|
|
import { ifInBrowser, uuid } from '@midscene/shared/utils';
|
2024-11-19 16:01:13 +08:00
|
|
|
import {
|
|
|
|
|
MIDSCENE_DEBUG_MODE,
|
|
|
|
|
MIDSCENE_OPENAI_INIT_CONFIG_JSON,
|
|
|
|
|
getAIConfig,
|
2024-12-10 09:24:21 +08:00
|
|
|
getAIConfigInJson,
|
2024-11-26 16:23:04 +08:00
|
|
|
} from './env';
|
2024-08-15 17:59:43 +08:00
|
|
|
import type { Rect, ReportDumpWithAttributes } from './types';
|
2024-07-23 16:25:11 +08:00
|
|
|
|
2025-03-07 17:20:18 +08:00
|
|
|
let logDir = path.join(process.cwd(), './midscene_run/');
|
2024-07-23 16:25:11 +08:00
|
|
|
let logEnvReady = false;
|
2024-07-25 13:40:46 +08:00
|
|
|
export const groupedActionDumpFileExt = 'web-dump.json';
|
2024-07-23 16:25:11 +08:00
|
|
|
|
2024-08-15 17:59:43 +08:00
|
|
|
export function getLogDir() {
|
2024-07-23 16:25:11 +08:00
|
|
|
return logDir;
|
|
|
|
|
}
|
2024-08-01 15:46:40 +08:00
|
|
|
|
2024-08-15 17:59:43 +08:00
|
|
|
export function setLogDir(dir: string) {
|
2024-07-23 16:25:11 +08:00
|
|
|
logDir = dir;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-25 16:05:01 +08:00
|
|
|
export function getLogDirByType(type: 'dump' | 'cache' | 'report' | 'tmp') {
|
2025-03-07 17:20:18 +08:00
|
|
|
if (ifInBrowser) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
const dir = path.join(getLogDir(), type);
|
2024-08-15 17:59:43 +08:00
|
|
|
if (!existsSync(dir)) {
|
|
|
|
|
mkdirSync(dir, { recursive: true });
|
|
|
|
|
}
|
|
|
|
|
return dir;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-05 11:49:21 +08:00
|
|
|
let reportTpl: string | null = null;
|
2025-03-24 09:50:27 +08:00
|
|
|
export function setReportTpl(tpl: string) {
|
|
|
|
|
reportTpl = tpl;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-05 11:49:21 +08:00
|
|
|
function getReportTpl() {
|
2025-03-24 09:50:27 +08:00
|
|
|
const globalScope = getGlobalScope();
|
|
|
|
|
if (
|
|
|
|
|
!reportTpl &&
|
|
|
|
|
globalScope &&
|
|
|
|
|
typeof (globalScope as any).get_midscene_report_tpl === 'function'
|
|
|
|
|
) {
|
|
|
|
|
reportTpl = (globalScope as any).get_midscene_report_tpl();
|
2024-11-05 11:49:21 +08:00
|
|
|
return reportTpl;
|
2024-10-28 11:04:40 +08:00
|
|
|
}
|
|
|
|
|
|
2025-03-09 21:50:20 +08:00
|
|
|
const __dirname = dirname(__filename);
|
2024-11-05 11:49:21 +08:00
|
|
|
if (!reportTpl) {
|
2025-04-01 15:03:42 +08:00
|
|
|
const possiblePaths = [
|
|
|
|
|
path.join(__dirname, '../../report/index.html'),
|
|
|
|
|
path.join(__dirname, '../report/index.html'),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const reportPath of possiblePaths) {
|
|
|
|
|
if (existsSync(reportPath)) {
|
|
|
|
|
reportTpl = readFileSync(reportPath, 'utf-8');
|
|
|
|
|
break;
|
|
|
|
|
}
|
2024-11-05 11:49:21 +08:00
|
|
|
}
|
2024-10-28 11:04:40 +08:00
|
|
|
}
|
2024-11-05 11:49:21 +08:00
|
|
|
return reportTpl;
|
|
|
|
|
}
|
2024-10-28 11:04:40 +08:00
|
|
|
|
2024-12-24 19:36:28 +08:00
|
|
|
export function replaceStringWithFirstAppearance(
|
|
|
|
|
str: string,
|
|
|
|
|
target: string,
|
|
|
|
|
replacement: string,
|
|
|
|
|
) {
|
|
|
|
|
const index = str.indexOf(target);
|
|
|
|
|
return str.slice(0, index) + replacement + str.slice(index + target.length);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-05 11:49:21 +08:00
|
|
|
export function reportHTMLContent(
|
|
|
|
|
dumpData: string | ReportDumpWithAttributes[],
|
|
|
|
|
): string {
|
|
|
|
|
const tpl = getReportTpl();
|
2025-01-20 20:02:49 +08:00
|
|
|
if (!tpl) {
|
|
|
|
|
console.warn('reportTpl is not set, will not write report');
|
|
|
|
|
return '';
|
|
|
|
|
}
|
2025-04-01 15:03:42 +08:00
|
|
|
|
2024-08-15 17:59:43 +08:00
|
|
|
let reportContent: string;
|
2024-08-31 08:17:50 +08:00
|
|
|
if (
|
|
|
|
|
(Array.isArray(dumpData) && dumpData.length === 0) ||
|
|
|
|
|
typeof dumpData === 'undefined'
|
|
|
|
|
) {
|
2024-12-24 19:36:28 +08:00
|
|
|
reportContent = replaceStringWithFirstAppearance(
|
|
|
|
|
tpl,
|
|
|
|
|
'{{dump}}',
|
2025-04-01 15:03:42 +08:00
|
|
|
'<script type="midscene_web_dump" type="application/json"></script>',
|
2024-08-31 08:17:50 +08:00
|
|
|
);
|
|
|
|
|
} else if (typeof dumpData === 'string') {
|
2024-12-24 19:36:28 +08:00
|
|
|
reportContent = replaceStringWithFirstAppearance(
|
|
|
|
|
tpl,
|
|
|
|
|
'{{dump}}',
|
2025-04-01 15:03:42 +08:00
|
|
|
// biome-ignore lint/style/useTemplate: <explanation>
|
|
|
|
|
'<script type="midscene_web_dump" type="application/json">\n' +
|
|
|
|
|
dumpData +
|
|
|
|
|
'\n</script>',
|
2024-08-15 17:59:43 +08:00
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
const dumps = dumpData.map(({ dumpString, attributes }) => {
|
|
|
|
|
const attributesArr = Object.keys(attributes || {}).map((key) => {
|
|
|
|
|
return `${key}="${encodeURIComponent(attributes![key])}"`;
|
|
|
|
|
});
|
2025-04-01 15:03:42 +08:00
|
|
|
return (
|
|
|
|
|
// biome-ignore lint/style/useTemplate: <explanation>
|
|
|
|
|
'<script type="midscene_web_dump" type="application/json" ' +
|
|
|
|
|
attributesArr.join(' ') +
|
|
|
|
|
'>\n' +
|
|
|
|
|
dumpString +
|
|
|
|
|
'\n</script>'
|
|
|
|
|
);
|
2024-08-15 17:59:43 +08:00
|
|
|
});
|
2024-12-24 19:36:28 +08:00
|
|
|
reportContent = replaceStringWithFirstAppearance(
|
|
|
|
|
tpl,
|
|
|
|
|
'{{dump}}',
|
|
|
|
|
dumps.join('\n'),
|
|
|
|
|
);
|
2024-08-15 17:59:43 +08:00
|
|
|
}
|
2024-11-05 11:49:21 +08:00
|
|
|
return reportContent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function writeDumpReport(
|
|
|
|
|
fileName: string,
|
|
|
|
|
dumpData: string | ReportDumpWithAttributes[],
|
|
|
|
|
): string | null {
|
|
|
|
|
if (ifInBrowser) {
|
|
|
|
|
console.log('will not write report in browser');
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-09 21:50:20 +08:00
|
|
|
const __dirname = dirname(__filename);
|
2024-11-05 11:49:21 +08:00
|
|
|
const midscenePkgInfo = getRunningPkgInfo(__dirname);
|
|
|
|
|
if (!midscenePkgInfo) {
|
|
|
|
|
console.warn('midscenePkgInfo not found, will not write report');
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-07 17:20:18 +08:00
|
|
|
const reportPath = path.join(getLogDirByType('report'), `${fileName}.html`);
|
2024-11-05 11:49:21 +08:00
|
|
|
const reportContent = reportHTMLContent(dumpData);
|
2025-01-20 20:02:49 +08:00
|
|
|
if (!reportContent) {
|
|
|
|
|
console.warn('reportContent is empty, will not write report');
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2024-08-15 17:59:43 +08:00
|
|
|
writeFileSync(reportPath, reportContent);
|
|
|
|
|
|
|
|
|
|
return reportPath;
|
2024-08-01 15:46:40 +08:00
|
|
|
}
|
|
|
|
|
|
2024-08-15 17:59:43 +08:00
|
|
|
export function writeLogFile(opts: {
|
2024-08-01 15:46:40 +08:00
|
|
|
fileName: string;
|
|
|
|
|
fileExt: string;
|
|
|
|
|
fileContent: string;
|
2024-11-25 16:05:01 +08:00
|
|
|
type: 'dump' | 'cache' | 'report' | 'tmp';
|
2024-08-15 17:59:43 +08:00
|
|
|
generateReport?: boolean;
|
2024-08-01 15:46:40 +08:00
|
|
|
}) {
|
2024-10-28 11:04:40 +08:00
|
|
|
if (ifInBrowser) {
|
|
|
|
|
return '/mock/report.html';
|
|
|
|
|
}
|
2024-08-01 15:46:40 +08:00
|
|
|
const { fileName, fileExt, fileContent, type = 'dump' } = opts;
|
2024-08-15 17:59:43 +08:00
|
|
|
const targetDir = getLogDirByType(type);
|
2024-07-23 16:25:11 +08:00
|
|
|
// Ensure directory exists
|
|
|
|
|
if (!logEnvReady) {
|
2024-08-01 15:46:40 +08:00
|
|
|
assert(targetDir, 'logDir should be set before writing dump file');
|
2024-07-23 16:25:11 +08:00
|
|
|
|
|
|
|
|
// gitIgnore in the parent directory
|
2025-03-07 17:20:18 +08:00
|
|
|
const gitIgnorePath = path.join(targetDir, '../../.gitignore');
|
2024-07-23 16:25:11 +08:00
|
|
|
let gitIgnoreContent = '';
|
|
|
|
|
if (existsSync(gitIgnorePath)) {
|
|
|
|
|
gitIgnoreContent = readFileSync(gitIgnorePath, 'utf-8');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ignore the log folder
|
2025-03-07 17:20:18 +08:00
|
|
|
const logDirName = path.basename(logDir);
|
2024-07-23 16:25:11 +08:00
|
|
|
if (!gitIgnoreContent.includes(`${logDirName}/`)) {
|
|
|
|
|
writeFileSync(
|
|
|
|
|
gitIgnorePath,
|
2025-02-07 14:55:52 +08:00
|
|
|
`${gitIgnoreContent}\n# Midscene.js dump files\n${logDirName}/report\n${logDirName}/tmp\n`,
|
2024-07-23 16:25:11 +08:00
|
|
|
'utf-8',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
logEnvReady = true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-07 17:20:18 +08:00
|
|
|
const filePath = path.join(targetDir, `${fileName}.${fileExt}`);
|
2024-08-22 20:56:34 +08:00
|
|
|
|
2025-02-07 14:55:52 +08:00
|
|
|
if (type !== 'dump') {
|
|
|
|
|
// do not write dump file any more
|
2025-03-07 17:20:18 +08:00
|
|
|
const outputResourceDir = path.dirname(filePath);
|
2025-02-07 14:55:52 +08:00
|
|
|
if (!existsSync(outputResourceDir)) {
|
|
|
|
|
mkdirSync(outputResourceDir, { recursive: true });
|
|
|
|
|
}
|
2024-08-22 20:56:34 +08:00
|
|
|
|
2025-02-07 14:55:52 +08:00
|
|
|
writeFileSync(filePath, fileContent);
|
|
|
|
|
}
|
2024-08-01 15:46:40 +08:00
|
|
|
|
2024-08-15 17:59:43 +08:00
|
|
|
if (opts?.generateReport) {
|
|
|
|
|
return writeDumpReport(fileName, fileContent);
|
2024-08-01 15:46:40 +08:00
|
|
|
}
|
2024-07-23 16:25:11 +08:00
|
|
|
|
|
|
|
|
return filePath;
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-28 11:04:40 +08:00
|
|
|
export function getTmpDir(): string | null {
|
2025-03-07 17:20:18 +08:00
|
|
|
try {
|
|
|
|
|
const runningPkgInfo = getRunningPkgInfo();
|
|
|
|
|
if (!runningPkgInfo) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const { name } = runningPkgInfo;
|
|
|
|
|
const tmpPath = path.join(tmpdir(), name);
|
|
|
|
|
mkdirSync(tmpPath, { recursive: true });
|
|
|
|
|
return tmpPath;
|
|
|
|
|
} catch (e) {
|
2024-10-28 11:04:40 +08:00
|
|
|
return null;
|
|
|
|
|
}
|
2024-07-23 16:25:11 +08:00
|
|
|
}
|
|
|
|
|
|
2024-10-28 11:04:40 +08:00
|
|
|
export function getTmpFile(fileExtWithoutDot: string): string | null {
|
|
|
|
|
if (ifInBrowser) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const tmpDir = getTmpDir();
|
|
|
|
|
const filename = `${uuid()}.${fileExtWithoutDot}`;
|
2025-03-07 17:20:18 +08:00
|
|
|
return path.join(tmpDir!, filename);
|
2024-07-23 16:25:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function overlapped(container: Rect, target: Rect) {
|
|
|
|
|
// container and the target have some part overlapped
|
|
|
|
|
return (
|
|
|
|
|
container.left < target.left + target.width &&
|
|
|
|
|
container.left + container.width > target.left &&
|
|
|
|
|
container.top < target.top + target.height &&
|
|
|
|
|
container.top + container.height > target.top
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function sleep(ms: number) {
|
|
|
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-08 15:38:26 +08:00
|
|
|
export function replacerForPageObject(key: string, value: any) {
|
|
|
|
|
if (value && value.constructor?.name === 'Page') {
|
|
|
|
|
return '[Page object]';
|
|
|
|
|
}
|
|
|
|
|
if (value && value.constructor?.name === 'Browser') {
|
|
|
|
|
return '[Browser object]';
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function stringifyDumpData(data: any, indents?: number) {
|
|
|
|
|
return JSON.stringify(data, replacerForPageObject, indents);
|
|
|
|
|
}
|
2024-10-28 11:04:40 +08:00
|
|
|
|
|
|
|
|
declare const __VERSION__: string;
|
|
|
|
|
|
|
|
|
|
export function getVersion() {
|
|
|
|
|
return __VERSION__;
|
|
|
|
|
}
|
2024-11-19 16:01:13 +08:00
|
|
|
|
|
|
|
|
function debugLog(...message: any[]) {
|
|
|
|
|
const debugMode = getAIConfig(MIDSCENE_DEBUG_MODE);
|
|
|
|
|
if (debugMode) {
|
|
|
|
|
console.log('[Midscene]', ...message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let lastReportedRepoUrl = '';
|
2024-12-10 09:24:21 +08:00
|
|
|
export function uploadTestInfoToServer({ testUrl }: { testUrl: string }) {
|
2024-11-19 16:01:13 +08:00
|
|
|
let repoUrl = '';
|
2024-11-19 18:04:36 +08:00
|
|
|
let userEmail = '';
|
2024-11-19 16:01:13 +08:00
|
|
|
|
2024-12-10 09:24:21 +08:00
|
|
|
const extraConfig = getAIConfigInJson(MIDSCENE_OPENAI_INIT_CONFIG_JSON);
|
2024-11-19 16:01:13 +08:00
|
|
|
const serverUrl = extraConfig?.REPORT_SERVER_URL;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
repoUrl = execSync('git config --get remote.origin.url').toString().trim();
|
2024-11-19 18:04:36 +08:00
|
|
|
userEmail = execSync('git config --get user.email').toString().trim();
|
2024-11-19 16:01:13 +08:00
|
|
|
} catch (error) {
|
2024-11-19 18:04:36 +08:00
|
|
|
debugLog('Failed to get git info:', error);
|
2024-11-19 16:01:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only upload test info if:
|
|
|
|
|
// 1. Server URL is configured AND
|
|
|
|
|
// 2. Either:
|
|
|
|
|
// - We have a repo URL that's different from last reported one (to avoid duplicate reports)
|
|
|
|
|
// - OR we don't have a repo URL but have a test URL (for non-git environments)
|
|
|
|
|
if (
|
|
|
|
|
serverUrl &&
|
|
|
|
|
((repoUrl && repoUrl !== lastReportedRepoUrl) || (!repoUrl && testUrl))
|
|
|
|
|
) {
|
|
|
|
|
debugLog('Uploading test info to server', {
|
|
|
|
|
serverUrl,
|
|
|
|
|
repoUrl,
|
|
|
|
|
testUrl,
|
2024-11-19 18:04:36 +08:00
|
|
|
userEmail,
|
2024-11-19 16:01:13 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fetch(serverUrl, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
repo_url: repoUrl,
|
|
|
|
|
test_url: testUrl,
|
2024-11-19 18:04:36 +08:00
|
|
|
user_email: userEmail,
|
2024-11-19 16:01:13 +08:00
|
|
|
}),
|
|
|
|
|
})
|
|
|
|
|
.then((response) => response.json())
|
|
|
|
|
.then((data) => {
|
|
|
|
|
debugLog('Successfully uploaded test info to server:', data);
|
|
|
|
|
})
|
|
|
|
|
.catch((error) =>
|
|
|
|
|
debugLog('Failed to upload test info to server:', error),
|
|
|
|
|
);
|
|
|
|
|
lastReportedRepoUrl = repoUrl;
|
|
|
|
|
}
|
|
|
|
|
}
|