refactor(chrome-devtool): extract the chrome-devtool logic into an application, support local development debugging, and add contribution guidelines. (#476)
* chore: add chrome devtools app * chore: resolve import error * chore: support visualizer css * add build logic * chore: add build extension zip file script * chore: migrate part of chrome extension content to app * chore: delete unless file * chore: optimize chrome devtool build script * chore: optimize chrome devtool build script * fix: resolve bridge mode test issues * chore: optimize chrome devtool build script * chore: optimize chrome devtool build script * chore: optimize chrome devtool build script * chore: update chrome devtools build process * chore: optimize chrome devtool build script * chore: optimize chrome devtool build script * chore: optimize chrome devtool build script * chore: optimize chrome devtool build script
3
.github/workflows/release.yml
vendored
@ -61,4 +61,5 @@ jobs:
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: chrome_extension
|
||||
path: ${{ github.workspace }}/packages/visualizer/dist/extension
|
||||
path: ${{ github.workspace }}/apps/chrome-extension/extension_output
|
||||
|
||||
2
.gitignore
vendored
@ -107,3 +107,5 @@ __ai_responses__/
|
||||
midscene_run
|
||||
midscene_run/report
|
||||
midscene_run/dump
|
||||
|
||||
extension_output
|
||||
@ -228,3 +228,67 @@ Here are the steps to publish (we generally use CI for releases and avoid publis
|
||||
1. [Run the release action](https://github.com/web-infra-dev/midscene/actions/workflows/release.yml).
|
||||
2. [Generate the release notes](https://github.com/web-infra-dev/midscene/releases).
|
||||
|
||||
## Chrome DevTools Extension
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
midscene/
|
||||
├── apps/
|
||||
│ ├── chrome-extension/ # Chrome extension application
|
||||
│ │ ├── dist/ # Build output directory
|
||||
│ │ ├── extension/ # Packaged Chrome extension directory
|
||||
│ │ ├── scripts/ # Build and utility scripts
|
||||
│ │ ├── src/ # Source code
|
||||
│ │ │ ├── extension/ # Chrome extension-specific code
|
||||
│ │ │ └── ...
|
||||
│ │ ├── static/ # Static resources
|
||||
│ │ └── ...
|
||||
│ └── ...
|
||||
├── packages/
|
||||
│ ├── core/ # Core functionality
|
||||
│ ├── visualizer/ # Visualization components
|
||||
│ ├── web-integration/ # Web integration
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Developing the Chrome DevTools Extension
|
||||
|
||||
The Chrome DevTools extension uses the Rsbuild build system. Development workflow is as follows:
|
||||
|
||||
1. **Build base packages**:
|
||||
```sh
|
||||
# First build the base packages
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
2. **Development mode**:
|
||||
```sh
|
||||
# Navigate to chrome-extension directory
|
||||
cd apps/chrome-extension
|
||||
|
||||
# Start the development server
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
3. **Build the extension**:
|
||||
```sh
|
||||
# Build the Chrome extension
|
||||
cd apps/chrome-extension
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
4. **Install the extension**:
|
||||
|
||||
The built `dist` directory can be directly installed as a Chrome extension. In Chrome browser:
|
||||
- Open `chrome://extensions/`
|
||||
- Enable "Developer mode" in the top-right corner
|
||||
- Click "Load unpacked" in the top-left corner
|
||||
- Select the `apps/chrome-extension/dist` directory
|
||||
|
||||
Alternatively, you can use the packaged extension:
|
||||
- Select the `apps/chrome-extension/extension_output/midscene-extension-v{version}.zip` file
|
||||
|
||||
For more detailed information, please refer to [Chrome DevTools README](./apps/chrome-extension/README.md).
|
||||
|
||||
|
||||
150
apps/chrome-extension/README.md
Normal file
@ -0,0 +1,150 @@
|
||||
# Midscene Chrome DevTools
|
||||
|
||||
Chrome extension version of the Midscene tool, providing browser automation, Bridge mode, and a Playground testing environment.
|
||||
|
||||
## Development Guide
|
||||
|
||||
### Environment Setup
|
||||
|
||||
Make sure you have completed the basic environment setup according to the main project's [Contribution Guide](../../CONTRIBUTING.md).
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
chrome-extension/
|
||||
├── dist/ # Build output directory, can be directly installed as a Chrome extension
|
||||
├── extension/ # Packaged Chrome extension
|
||||
│ └── midscene-extension-v{version}.zip # Compressed extension
|
||||
├── scripts/ # Build and utility scripts
|
||||
│ ├── build-report-template.js # Generate report template
|
||||
│ └── pack-extension.js # Package Chrome extension
|
||||
├── src/ # Source code
|
||||
│ ├── extension/ # Chrome extension-related components
|
||||
│ │ ├── bridge.tsx # Bridge mode UI
|
||||
│ │ ├── popup.tsx # Extension popup homepage
|
||||
│ │ ├── misc.tsx # Auxiliary components
|
||||
│ │ ├── utils.ts # Utility functions
|
||||
│ │ ├── common.less # Common style variables
|
||||
│ │ ├── popup.less # Popup styles
|
||||
│ │ └── bridge.less # Bridge mode styles
|
||||
│ ├── blank_polyfill.ts # Browser polyfill for Node.js modules
|
||||
│ ├── index.tsx # Main entry
|
||||
│ └── App.tsx # Main application component
|
||||
├── static/ # Static resources directory, will be copied to the dist directory
|
||||
│ └── scripts/ # Script resources
|
||||
│ └── report-template.js # Generated report template
|
||||
├── package.json # Project configuration
|
||||
├── rsbuild.config.ts # Rsbuild build configuration
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Development Process
|
||||
|
||||
1. **Install Dependencies**
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. **Build Dependency Packages**
|
||||
```bash
|
||||
# Build all packages in the project root
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
3. **Development Mode**
|
||||
```bash
|
||||
# Start the project in development mode
|
||||
cd apps/chrome-extension
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
4. **Build Project**
|
||||
```bash
|
||||
# Build the Chrome extension
|
||||
cd apps/chrome-extension
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
The build process includes:
|
||||
- Building the web application using rsbuild
|
||||
- Generating the report template script (report-template.js)
|
||||
- Packaging the build artifacts as a Chrome extension
|
||||
|
||||
### Installing the Extension
|
||||
|
||||
#### Method 1: Using the dist directory (for development and debugging)
|
||||
|
||||
The built `dist` directory can be directly installed as a Chrome extension:
|
||||
1. Open Chrome browser, navigate to `chrome://extensions/`
|
||||
2. Enable "Developer mode" in the top-right corner
|
||||
3. Click "Load unpacked" in the top-left corner
|
||||
4. Select the `apps/chrome-extension/dist` directory
|
||||
|
||||
This method is suitable for quick testing during development.
|
||||
|
||||
#### Method 2: Using the packaged extension file
|
||||
|
||||
For publishing or sharing:
|
||||
1. Use the `pnpm run build` command to build the project
|
||||
2. Find the `midscene-extension-v{version}.zip` file in the `extension` directory
|
||||
3. Upload this file to the Chrome Web Store developer console, or share it with others for installation
|
||||
|
||||
### Debugging Tips
|
||||
|
||||
#### Debugging the Extension Background
|
||||
|
||||
1. Find the Midscene extension on the Chrome extensions page (`chrome://extensions/`)
|
||||
2. Click the "view: background page" link to open the developer tools
|
||||
3. Use the console and network panels for debugging
|
||||
|
||||
#### Debugging the Popup Window
|
||||
|
||||
1. Click the Midscene icon in the Chrome toolbar to open the extension popup
|
||||
2. Right-click on the popup and select "Inspect"
|
||||
3. Use the developer tools to debug UI and interactions
|
||||
|
||||
#### Debugging Content Scripts
|
||||
|
||||
1. Open any webpage, click the Midscene icon to activate the extension
|
||||
2. Open the developer tools
|
||||
3. Find the Midscene scripts in the "Content scripts" section under the "Sources" panel
|
||||
|
||||
### Feature Description
|
||||
|
||||
#### Report Template
|
||||
|
||||
The Chrome extension uses the HTML report template from the `@midscene/visualizer` package. During the build process, it:
|
||||
- Reads `packages/visualizer/dist/report/index.html`
|
||||
- Converts its content to a JavaScript string
|
||||
- Creates a JS file containing the `get_midscene_report_tpl()` function
|
||||
- Saves it to `static/scripts/report-template.js`
|
||||
|
||||
#### Bridge Mode
|
||||
|
||||
Bridge mode allows controlling the browser from a local terminal via the Midscene SDK. This is useful for operating the browser through both scripts and manual interaction, or for reusing cookies.
|
||||
|
||||
## Release Process
|
||||
|
||||
1. Update the version number in `package.json` to match the main project
|
||||
2. Run the build: `pnpm run build`
|
||||
3. Verify the `midscene-extension-v{version}.zip` file generated in the `extension` directory
|
||||
4. Submit the ZIP file to the Chrome Web Store
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Report template generation failure**
|
||||
- Make sure to build the `@midscene/visualizer` package first
|
||||
- Check if `packages/visualizer/dist/report/index.html` exists
|
||||
|
||||
2. **React Hooks errors**
|
||||
- Check for multiple React instances, might need to adjust the externals configuration in `rsbuild.config.ts`
|
||||
|
||||
3. **async_hooks module not found**
|
||||
- Check the alias configuration in `rsbuild.config.ts` to ensure it points correctly to the polyfill file
|
||||
|
||||
4. **Extension doesn't work properly after installation**
|
||||
- Check for error messages in the Chrome console
|
||||
- Verify that the build process was executed completely
|
||||
- Validate the permissions configuration in the manifest.json file
|
||||
33
apps/chrome-extension/package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "chrome-extension",
|
||||
"private": true,
|
||||
"version": "0.12.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "rsbuild build && npm run build:report-template && npm run pack-extension",
|
||||
"build:report-template": "node scripts/build-report-template.js",
|
||||
"dev": "rsbuild dev --open",
|
||||
"preview": "rsbuild preview",
|
||||
"pack-extension": "node scripts/pack-extension.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.3.1",
|
||||
"@midscene/visualizer": "workspace:*",
|
||||
"@midscene/web": "workspace:*",
|
||||
"antd": "5.21.6",
|
||||
"dayjs": "^1.11.11",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rsbuild/core": "^1.2.16",
|
||||
"@rsbuild/plugin-less": "^1.1.1",
|
||||
"@rsbuild/plugin-node-polyfill": "1.3.0",
|
||||
"@rsbuild/plugin-react": "^1.1.1",
|
||||
"@types/chrome": "0.0.279",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"less": "^4.2.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
82
apps/chrome-extension/rsbuild.config.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import path from 'node:path';
|
||||
import { defineConfig } from '@rsbuild/core';
|
||||
import { pluginLess } from '@rsbuild/plugin-less';
|
||||
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill';
|
||||
import { pluginReact } from '@rsbuild/plugin-react';
|
||||
import { version } from '../../packages/visualizer/package.json';
|
||||
|
||||
export default defineConfig({
|
||||
environments: {
|
||||
web: {
|
||||
source: {
|
||||
entry: {
|
||||
index: './src/index.tsx',
|
||||
popup: './src/extension/popup.tsx',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
target: 'web',
|
||||
sourceMap: true,
|
||||
},
|
||||
html: {
|
||||
tags: [
|
||||
{
|
||||
tag: 'script',
|
||||
attrs: { src: 'scripts/report-template.js' },
|
||||
head: true,
|
||||
append: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
node: {
|
||||
source: {
|
||||
entry: {
|
||||
worker: './src/scripts/worker.ts',
|
||||
'stop-water-flow': './src/scripts/stop-water-flow.ts',
|
||||
'water-flow': './src/scripts/water-flow.ts',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
target: 'node',
|
||||
sourceMap: true,
|
||||
filename: {
|
||||
js: 'scripts/[name].js',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dev: {
|
||||
writeToDisk: true,
|
||||
},
|
||||
output: {
|
||||
polyfill: 'entry',
|
||||
copy: [
|
||||
{ from: './static', to: './' },
|
||||
{
|
||||
from: path.resolve(
|
||||
__dirname,
|
||||
'../../packages/web-integration/iife-script',
|
||||
),
|
||||
to: 'scripts',
|
||||
},
|
||||
],
|
||||
},
|
||||
source: {
|
||||
define: {
|
||||
__SDK_VERSION__: JSON.stringify(version),
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
async_hooks: path.join(__dirname, './src/scripts/blank_polyfill.ts'),
|
||||
'node:async_hooks': path.join(
|
||||
__dirname,
|
||||
'./src/scripts/blank_polyfill.ts',
|
||||
),
|
||||
react: path.resolve(__dirname, 'node_modules/react'),
|
||||
'react-dom': path.resolve(__dirname, 'node_modules/react-dom'),
|
||||
},
|
||||
},
|
||||
plugins: [pluginReact(), pluginNodePolyfill(), pluginLess()],
|
||||
});
|
||||
53
apps/chrome-extension/scripts/build-report-template.js
Normal file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// Get the directory path of the current file
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Project root directory
|
||||
const projectRoot = path.resolve(__dirname, '../../..');
|
||||
|
||||
// Path configuration
|
||||
const visualizerReportPath = path.join(
|
||||
projectRoot,
|
||||
'packages/visualizer/dist/report/index.html',
|
||||
);
|
||||
const outputDir = path.join(__dirname, '../dist/scripts');
|
||||
const outputFile = path.join(outputDir, 'report-template.js');
|
||||
|
||||
// Ensure the output directory exists
|
||||
console.log(`Creating output directory: ${outputDir}`);
|
||||
fs.mkdirSync(outputDir, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
// Check if the visualizer has been built
|
||||
if (!fs.existsSync(visualizerReportPath)) {
|
||||
console.error(
|
||||
`ERROR: Report template file not found at ${visualizerReportPath}`,
|
||||
);
|
||||
console.error(
|
||||
'Make sure to build the visualizer package first with: npm run build -w @midscene/visualizer',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Read the report template HTML
|
||||
console.log(`Reading report template from: ${visualizerReportPath}`);
|
||||
const reportHtml = fs.readFileSync(visualizerReportPath, 'utf8');
|
||||
|
||||
// Create JavaScript function
|
||||
const jsContent = `
|
||||
// Generated report template from visualizer
|
||||
window.get_midscene_report_tpl = function() {
|
||||
return ${JSON.stringify(reportHtml)};
|
||||
};
|
||||
`;
|
||||
|
||||
// Write to file
|
||||
fs.writeFileSync(outputFile, jsContent);
|
||||
console.log(`Report template successfully written to: ${outputFile}`);
|
||||
81
apps/chrome-extension/scripts/pack-extension.js
Executable file
@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
fileURLToPath
|
||||
} from 'node:url';
|
||||
import archiver from 'archiver';
|
||||
|
||||
// Get the directory path of the current file
|
||||
const __filename = fileURLToPath(
|
||||
import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Read package.json
|
||||
const packageJsonPath = path.resolve(__dirname, '../package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
|
||||
// Validate version string to prevent injection
|
||||
const version = packageJson.version;
|
||||
if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9.]+)?$/.test(version)) {
|
||||
console.error('Invalid version format in package.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create extension directory
|
||||
const extensionDir = path.resolve(__dirname, '../extension_output');
|
||||
if (!fs.existsSync(extensionDir)) {
|
||||
fs.mkdirSync(extensionDir, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Source directory - dist
|
||||
const distDir = path.resolve(__dirname, '../dist');
|
||||
|
||||
// Create zip file
|
||||
const zipFileName = `midscene-extension-v${version}.zip`;
|
||||
const zipFilePath = path.resolve(extensionDir, zipFileName);
|
||||
|
||||
// Delete existing zip file
|
||||
if (fs.existsSync(zipFilePath)) {
|
||||
fs.unlinkSync(zipFilePath);
|
||||
}
|
||||
|
||||
// Create a file to stream archive data to
|
||||
const output = fs.createWriteStream(zipFilePath);
|
||||
const archive = archiver('zip', {
|
||||
zlib: {
|
||||
level: 9
|
||||
} // Sets the compression level
|
||||
});
|
||||
|
||||
// Listen for all archive data to be written
|
||||
output.on('close', () => {
|
||||
console.log(`Extension packed successfully: ${zipFileName} (${archive.pointer()} total bytes saved in extension directory)`);
|
||||
});
|
||||
|
||||
// Handle warnings and errors
|
||||
archive.on('warning', (err) => {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.warn('Warning during archiving:', err);
|
||||
} else {
|
||||
console.error('Error during archiving:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
archive.on('error', (err) => {
|
||||
console.error('Error during archiving:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Pipe archive data to the file
|
||||
archive.pipe(output);
|
||||
|
||||
// Append files from dist directory, putting files at the root of archive
|
||||
archive.directory(distDir, false);
|
||||
|
||||
// Finalize the archive (i.e. we are done appending files but streams have to finish yet)
|
||||
archive.finalize();
|
||||
397
apps/chrome-extension/src/App.css
Normal file
@ -0,0 +1,397 @@
|
||||
/* src/extension/popup.less */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
|
||||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
font-size: 14px;
|
||||
}
|
||||
.popup-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 20px 0;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.popup-wrapper .tabs-container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.popup-wrapper .ant-tabs-nav {
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.popup-wrapper .popup-header {
|
||||
padding: 0 20px;
|
||||
}
|
||||
.popup-wrapper .hr {
|
||||
border-top: 1px solid #e5e5e5;
|
||||
margin-bottom: 15px;
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.popup-wrapper .popup-playground-container,
|
||||
.popup-wrapper .popup-bridge-container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.popup-wrapper .popup-bridge-container {
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.popup-wrapper .popup-footer {
|
||||
color: #ccc;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* src/component/logo.less */
|
||||
.logo img {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
vertical-align: baseline;
|
||||
vertical-align: -webkit-baseline-middle;
|
||||
}
|
||||
.logo-with-star-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.logo-with-star-wrapper .github-star {
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
/* src/component/blackboard.less */
|
||||
.blackboard .footer {
|
||||
color: #aaa;
|
||||
}
|
||||
.blackboard ul {
|
||||
padding-left: 0px;
|
||||
}
|
||||
.blackboard li {
|
||||
list-style: none;
|
||||
}
|
||||
.blackboard .bottom-tip {
|
||||
height: 30px;
|
||||
}
|
||||
.blackboard .bottom-tip-item {
|
||||
max-width: 500px;
|
||||
color: #aaa;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.blackboard-filter {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.blackboard-main-content canvas {
|
||||
width: 100%;
|
||||
border: 1px solid #888;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* src/component/player.less */
|
||||
.player-container {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
padding: 12px 0;
|
||||
padding-bottom: 0;
|
||||
background: #434443dd;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #979797;
|
||||
border-radius: 6px;
|
||||
line-height: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.player-container .canvas-container {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.player-container .canvas-container canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.player-container .player-timeline {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: #666;
|
||||
position: relative;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.player-container .player-timeline .player-timeline-progress {
|
||||
transition-timing-function: linear;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 4px;
|
||||
background: #06b1ab;
|
||||
}
|
||||
.player-container .player-tools {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0 12px;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.player-container .player-tools .player-control {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: left;
|
||||
}
|
||||
.player-container .player-tools .status-icon {
|
||||
transition: 0.2s;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 12px;
|
||||
background: #666;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.player-container .player-tools .status-text {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.player-container .player-tools .title {
|
||||
font-weight: bold;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.player-container .player-tools .subtitle {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: 0;
|
||||
}
|
||||
.player-container .player-tools .player-tools-item {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.player-container .player-tools .player-tools-item .ant-btn-variant-link {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* src/component/playground-component.less */
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
|
||||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
font-size: 14px;
|
||||
}
|
||||
.playground-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.playground-container.vertical-mode {
|
||||
height: inherit;
|
||||
}
|
||||
.playground-container.vertical-mode .form-part {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.playground-container.vertical-mode .form-part h3 {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.playground-container .playground-header {
|
||||
padding: 10px 10px 30px;
|
||||
}
|
||||
.playground-container .playground-left-panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
.playground-container .playground-left-panel .ant-form {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.playground-container .form-part {
|
||||
margin-bottom: 20px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.playground-container .form-part h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.playground-container .form-part .switch-btn-wrapper {
|
||||
margin-left: 2px;
|
||||
}
|
||||
.playground-container .input-wrapper {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.playground-container .input-wrapper .main-side-console-input {
|
||||
position: relative;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.playground-container .input-wrapper .ant-input {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
.playground-container .input-wrapper .form-controller-wrapper {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
padding: 0 12px;
|
||||
left: 0px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.playground-container .input-wrapper .settings-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2px;
|
||||
color: #777;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.playground-container .input-wrapper .settings-wrapper.settings-wrapper-hover {
|
||||
color: #3b3b3b;
|
||||
}
|
||||
.playground-container .input-wrapper .history-selector {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.playground-container .loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 20px 20px;
|
||||
}
|
||||
.playground-container .loading-container .loading-progress-text {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
color: #777;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.playground-container .loading-container .loading-progress-text-tab-info {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.playground-container .loading-container .loading-progress-text-progress {
|
||||
height: 60px;
|
||||
}
|
||||
.playground-container .result-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
padding: 20px 20px;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.playground-container .result-wrapper.result-wrapper-compact {
|
||||
padding: 0;
|
||||
}
|
||||
.playground-container .result-wrapper.vertical-mode-result {
|
||||
height: inherit;
|
||||
min-height: 300px;
|
||||
}
|
||||
.playground-container .result-wrapper .result-empty-tip {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
color: #ccc;
|
||||
}
|
||||
.playground-container .result-wrapper pre {
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* src/component/open-in-playground.less */
|
||||
.playground-drawer .ant-drawer-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* src/extension/bridge.less */
|
||||
.bridge-status-bar {
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.bridge-status-bar .bridge-status-text {
|
||||
flex-grow: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.bridge-status-bar .bridge-status-text .bridge-status-tip {
|
||||
margin-left: 6px;
|
||||
flex-grow: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bridge-status-bar .bridge-status-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.bridge-log-container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.bridge-log-container .bridge-log-item-content {
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
7
apps/chrome-extension/src/App.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import './App.css';
|
||||
import { PlaygroundPopup } from './extension/popup';
|
||||
|
||||
export default function App() {
|
||||
return <PlaygroundPopup />;
|
||||
}
|
||||
1
apps/chrome-extension/src/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="@rsbuild/core/types" />
|
||||
@ -1,4 +1,4 @@
|
||||
@import '../component/common.less';
|
||||
@import './common.less';
|
||||
|
||||
.bridge-status-bar {
|
||||
height: 56px;
|
||||
@ -47,4 +47,4 @@
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { ExtensionBridgePageBrowserSide } from '@midscene/web/bridge-mode-browser';
|
||||
import { Button, Spin } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import './bridge.less';
|
||||
import { iconForStatus } from '@/component/misc';
|
||||
import dayjs from 'dayjs';
|
||||
import { iconForStatus } from './misc';
|
||||
|
||||
interface BridgeLogItem {
|
||||
time: string;
|
||||
@ -250,7 +250,9 @@ export default function Bridge() {
|
||||
clear
|
||||
</Button>
|
||||
</h3>
|
||||
<div className="bridge-log-container">{logs}</div>
|
||||
<div className="bridge-log-container">
|
||||
{logs.length === 0 ? <p>No logs yet</p> : logs}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
26
apps/chrome-extension/src/extension/common.less
Normal file
@ -0,0 +1,26 @@
|
||||
@main-text: #3b3b3b;
|
||||
|
||||
@primary-color: #06b1ab;
|
||||
@main-orange: #F9483E;
|
||||
|
||||
@side-bg: #F8F8F8;
|
||||
@title-bg: @side-bg;
|
||||
@border-color: #E5E5E5;
|
||||
@heavy-border-color: #888;
|
||||
|
||||
@selected-bg: #bfc4da80;
|
||||
@hover-bg: #dcdcdc80;
|
||||
|
||||
@weak-bg: #F3F3F3;
|
||||
@weak-text: #777;
|
||||
@footer-text: #CCC;
|
||||
|
||||
@toolbar-btn-bg: #E9E9E9;
|
||||
|
||||
@layout-space: 20px;
|
||||
|
||||
@side-horizontal-padding: 10px;
|
||||
@side-vertical-spacing: 10px;
|
||||
|
||||
@layout-extension-space-horizontal: 20px;
|
||||
@layout-extension-space-vertical: 20px;
|
||||
49
apps/chrome-extension/src/extension/misc.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import {
|
||||
ArrowRightOutlined,
|
||||
CheckOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseOutlined,
|
||||
LogoutOutlined,
|
||||
MinusOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
|
||||
export const iconForStatus = (status: string): JSX.Element => {
|
||||
switch (status) {
|
||||
case 'finished':
|
||||
case 'passed':
|
||||
case 'success':
|
||||
case 'connected':
|
||||
return (
|
||||
<span style={{ color: '#2B8243' }}>
|
||||
<CheckOutlined />
|
||||
</span>
|
||||
);
|
||||
|
||||
case 'finishedWithWarning':
|
||||
return (
|
||||
<span style={{ color: '#f7bb05' }}>
|
||||
<WarningOutlined />
|
||||
</span>
|
||||
);
|
||||
case 'failed':
|
||||
case 'closed':
|
||||
case 'timedOut':
|
||||
case 'interrupted':
|
||||
return (
|
||||
<span style={{ color: '#FF0A0A' }}>
|
||||
<CloseOutlined />
|
||||
</span>
|
||||
);
|
||||
case 'pending':
|
||||
return <ClockCircleOutlined />;
|
||||
case 'cancelled':
|
||||
case 'skipped':
|
||||
return <LogoutOutlined />;
|
||||
case 'running':
|
||||
return <ArrowRightOutlined />;
|
||||
default:
|
||||
return <MinusOutlined />;
|
||||
}
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
@import '../component/common.less';
|
||||
@import './common.less';
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
@ -53,4 +53,4 @@ body {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,27 +1,22 @@
|
||||
/// <reference types="chrome" />
|
||||
import { ConfigProvider, Tabs } from 'antd';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { setSideEffect } from '../init';
|
||||
import './popup.less';
|
||||
|
||||
import { globalThemeConfig } from '@/component/color';
|
||||
import Logo from '@/component/logo';
|
||||
import { ApiOutlined, SendOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Logo,
|
||||
Playground,
|
||||
extensionAgentForTab,
|
||||
} from '@/component/playground-component';
|
||||
import { useEnvConfig } from '@/component/store';
|
||||
import { ApiOutlined, SendOutlined } from '@ant-design/icons';
|
||||
getExtensionVersion,
|
||||
globalThemeConfig,
|
||||
useEnvConfig,
|
||||
} from '@midscene/visualizer/extension';
|
||||
import { ConfigProvider, Tabs } from 'antd';
|
||||
import Bridge from './bridge';
|
||||
import { getExtensionVersion } from './utils';
|
||||
import './popup.less';
|
||||
|
||||
setSideEffect();
|
||||
declare const __SDK_VERSION__: string;
|
||||
|
||||
declare const __VERSION__: string;
|
||||
|
||||
function PlaygroundPopup() {
|
||||
export function PlaygroundPopup() {
|
||||
const extensionVersion = getExtensionVersion();
|
||||
const { popupTab, setPopupTab, forceSameTabNavigation } = useEnvConfig();
|
||||
const { popupTab, setPopupTab } = useEnvConfig();
|
||||
|
||||
const items = [
|
||||
{
|
||||
@ -76,56 +71,11 @@ function PlaygroundPopup() {
|
||||
|
||||
<div className="popup-footer">
|
||||
<p>
|
||||
Midscene.js Chrome Extension v{extensionVersion} (SDK v{__VERSION__}
|
||||
)
|
||||
Midscene.js Chrome Extension v{extensionVersion} (SDK v
|
||||
{__SDK_VERSION__})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const element = document.getElementById('root');
|
||||
if (element) {
|
||||
const root = ReactDOM.createRoot(element);
|
||||
root.render(<PlaygroundPopup />);
|
||||
}
|
||||
|
||||
// const shotAndOpenPlayground = async (
|
||||
// agent?: ChromeExtensionProxyPageAgent | null,
|
||||
// ) => {
|
||||
// if (!agent) {
|
||||
// message.error('No agent found');
|
||||
// return;
|
||||
// }
|
||||
// const context = await agent.getUIContext();
|
||||
|
||||
// // cache screenshot when page is active
|
||||
// const { id } = await sendToWorker<
|
||||
// WorkerRequestSaveContext,
|
||||
// WorkerResponseSaveContext
|
||||
// >(workerMessageTypes.SAVE_CONTEXT, {
|
||||
// context,
|
||||
// });
|
||||
// const url = getPlaygroundUrl(id);
|
||||
// chrome.tabs.create({
|
||||
// url,
|
||||
// active: true,
|
||||
// });
|
||||
// };
|
||||
|
||||
// const handleSendToPlayground = async () => {
|
||||
// if (!tabId || !windowId) {
|
||||
// message.error('No active tab or window found');
|
||||
// return;
|
||||
// }
|
||||
// setLoading(true);
|
||||
// try {
|
||||
// const agent = extensionAgentForTab(tabId);
|
||||
// await shotAndOpenPlayground(agent);
|
||||
// await agent!.page.destroy();
|
||||
// } catch (e: any) {
|
||||
// message.error(e.message || 'Failed to launch Playground');
|
||||
// }
|
||||
// setLoading(false);
|
||||
// };
|
||||
13
apps/chrome-extension/src/index.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const rootEl = document.getElementById('root');
|
||||
if (rootEl) {
|
||||
const root = ReactDOM.createRoot(rootEl);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
}
|
||||
1
apps/chrome-extension/src/scripts/blank_polyfill.ts
Normal file
@ -0,0 +1 @@
|
||||
export default {};
|
||||
@ -5,7 +5,7 @@ import {
|
||||
type WorkerRequestGetContext,
|
||||
type WorkerRequestSaveContext,
|
||||
workerMessageTypes,
|
||||
} from './utils';
|
||||
} from '../utils';
|
||||
|
||||
// console-browserify won't work in worker, so we need to use globalThis.console
|
||||
const console = globalThis.console;
|
||||
78
apps/chrome-extension/src/utils.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/// <reference types="chrome" />
|
||||
import type { WebUIContext } from '@midscene/web/utils';
|
||||
|
||||
export const workerMessageTypes = {
|
||||
SAVE_CONTEXT: 'save-context',
|
||||
GET_CONTEXT: 'get-context',
|
||||
};
|
||||
|
||||
// save screenshot
|
||||
export interface WorkerRequestSaveContext {
|
||||
context: WebUIContext;
|
||||
}
|
||||
|
||||
export interface WorkerResponseSaveContext {
|
||||
id: string;
|
||||
}
|
||||
|
||||
// get screenshot
|
||||
export interface WorkerRequestGetContext {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface WorkerResponseGetContext {
|
||||
context: WebUIContext;
|
||||
}
|
||||
|
||||
export async function sendToWorker<Payload, Result = any>(
|
||||
type: string,
|
||||
payload: Payload,
|
||||
): Promise<Result> {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage({ type, payload }, (response) => {
|
||||
if (response.error) {
|
||||
reject(response.error);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getPlaygroundUrl(cacheContextId: string) {
|
||||
return chrome.runtime.getURL(
|
||||
`./pages/playground.html?cache_context_id=${cacheContextId}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function activeTab(): Promise<chrome.tabs.Tab> {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
if (tabs?.[0]) {
|
||||
resolve(tabs[0]);
|
||||
} else {
|
||||
reject(new Error('No active tab found'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function currentWindowId(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.windows.getCurrent((window) => {
|
||||
if (window?.id) {
|
||||
resolve(window.id);
|
||||
} else {
|
||||
reject(new Error('No active window found'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getExtensionVersion() {
|
||||
return chrome.runtime?.getManifest()?.version || 'unknown';
|
||||
}
|
||||
|
||||
export async function getTabInfo(tabId: number) {
|
||||
return await chrome.tabs.get(tabId);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@ -1,29 +1,22 @@
|
||||
{
|
||||
"name": "Midscene.js",
|
||||
"description": "Open-source SDK for automating web pages using natural language through AI.",
|
||||
"version": "0.41",
|
||||
"version": "0.42",
|
||||
"manifest_version": 3,
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"tabs",
|
||||
"sidePanel",
|
||||
"debugger"
|
||||
],
|
||||
"permissions": ["activeTab", "tabs", "sidePanel", "debugger"],
|
||||
"incognito": "split",
|
||||
"background": {
|
||||
"service_worker": "./lib/worker.js"
|
||||
"service_worker": "./scripts/worker.js"
|
||||
},
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"action": {
|
||||
"default_icon": "icon128.png",
|
||||
"default_title": "Midscene.js"
|
||||
},
|
||||
"side_panel": {
|
||||
"default_path": "./pages/sidepanel.html"
|
||||
"default_path": "./index.html"
|
||||
},
|
||||
"icons": {
|
||||
"128": "icon128.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
apps/chrome-extension/tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "ES2020"],
|
||||
"jsx": "react-jsx",
|
||||
"target": "ES2020",
|
||||
"skipLibCheck": true,
|
||||
"useDefineForClassFields": true,
|
||||
|
||||
/* modules */
|
||||
"module": "ESNext",
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
|
||||
/* type checking */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@ -33,7 +33,7 @@
|
||||
"@types/minimist": "1.2.5",
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/yargs": "17.0.32",
|
||||
"typescript": "~5.0.4",
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "3.0.5",
|
||||
"yargs": "17.7.2",
|
||||
"chalk": "4.1.2",
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
"@modern-js/module-tools": "2.60.6",
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/node-fetch": "2.6.11",
|
||||
"typescript": "~5.0.4",
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "3.0.5"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
"dotenv": "16.4.5",
|
||||
"playwright": "1.44.1",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"typescript": "~5.0.4",
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "3.0.5"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
"@types/debug": "4.1.12",
|
||||
"@types/node": "^18.0.0",
|
||||
"rimraf": "~3.0.2",
|
||||
"typescript": "~5.0.4",
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "3.0.5"
|
||||
},
|
||||
"sideEffects": [],
|
||||
|
||||
@ -36,6 +36,21 @@ export default defineConfig({
|
||||
platform: 'browser',
|
||||
outDir: 'dist',
|
||||
target: 'es2020',
|
||||
externals: [...externals],
|
||||
},
|
||||
{
|
||||
...commonConfig,
|
||||
alias: {
|
||||
async_hooks: path.join(__dirname, './src/blank_polyfill.ts'),
|
||||
},
|
||||
dts: false,
|
||||
input: {
|
||||
extension: 'src/extension.tsx',
|
||||
},
|
||||
platform: 'browser',
|
||||
outDir: 'dist',
|
||||
target: 'es2020',
|
||||
externals: [...externals, 'react', 'react-dom'],
|
||||
},
|
||||
{
|
||||
...commonConfig,
|
||||
@ -45,10 +60,6 @@ export default defineConfig({
|
||||
format: 'iife',
|
||||
dts: false,
|
||||
input: {
|
||||
'water-flow': 'src/extension/scripts/water-flow.ts',
|
||||
'stop-water-flow': 'src/extension/scripts/stop-water-flow.ts',
|
||||
popup: 'src/extension/popup.tsx',
|
||||
worker: 'src/extension/worker.ts',
|
||||
'playground-entry': 'src/extension/playground-entry.tsx',
|
||||
},
|
||||
platform: 'browser',
|
||||
|
||||
@ -6,6 +6,23 @@
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"main": "./dist/lib/index.js",
|
||||
"module": "./dist/es/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/lib/index.js"
|
||||
},
|
||||
"./popup": {
|
||||
"types": "./dist/types/extension/popup.d.ts",
|
||||
"default": "./dist/popup.js"
|
||||
},
|
||||
"./extension": {
|
||||
"types": "./dist/types/extension.d.ts",
|
||||
"default": "./dist/extension.js"
|
||||
},
|
||||
"./popup.css": {
|
||||
"default": "./dist/popup.css"
|
||||
}
|
||||
},
|
||||
"files": ["dist", "html", "README.md"],
|
||||
"watch": {
|
||||
"build": {
|
||||
@ -23,8 +40,12 @@
|
||||
"upgrade": "modern upgrade",
|
||||
"e2e": "node ../cli/bin/midscene ./scripts/midscene/"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "5.3.7",
|
||||
"@ant-design/icons": "^5.3.1",
|
||||
"@midscene/core": "workspace:*",
|
||||
"@midscene/shared": "workspace:*",
|
||||
"@midscene/web": "workspace:*",
|
||||
@ -35,9 +56,9 @@
|
||||
"@pixi/unsafe-eval": "7.4.2",
|
||||
"@types/chrome": "0.0.279",
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"antd": "5.21.6",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"antd": "^5.21.6",
|
||||
"dayjs": "1.11.11",
|
||||
"execa": "9.3.0",
|
||||
"http-server": "14.1.1",
|
||||
@ -45,12 +66,12 @@
|
||||
"pixi-filters": "6.0.5",
|
||||
"pixi.js": "8.1.1",
|
||||
"query-string": "9.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-resizable-panels": "2.0.22",
|
||||
"rimraf": "~3.0.2",
|
||||
"tsx": "4.19.2",
|
||||
"typescript": "~5.0.4",
|
||||
"typescript": "^5.8.2",
|
||||
"zustand": "4.5.2"
|
||||
},
|
||||
"sideEffects": ["**/*.css", "**/*.less", "**/*.sass", "**/*.scss"],
|
||||
|
||||
@ -123,43 +123,6 @@ function reportHTMLWithDump(
|
||||
return html;
|
||||
}
|
||||
|
||||
/* build task: extension */
|
||||
function buildExtension() {
|
||||
// clear everything in the extension page dir
|
||||
rmSync(outputExtensionPageDir, { recursive: true, force: true });
|
||||
ensureDirectoryExistence(outputExtensionSidepanel);
|
||||
|
||||
// write the set-report-tpl.js into the extension
|
||||
writeFileSync(
|
||||
join(__dirname, '../unpacked-extension/lib/set-report-tpl.js'),
|
||||
tplRetrieverFn,
|
||||
);
|
||||
|
||||
// playground.html
|
||||
const resultWithOutsource = tplReplacer(playgroundTpl, {
|
||||
css: `<style>\n${playgroundCSS}\n</style>\n`,
|
||||
js: `<script src="/lib/playground-entry.js"></script>`,
|
||||
bootstrap: '<!-- leave it empty -->', // the entry iife will mount by itself
|
||||
});
|
||||
writeFileSync(
|
||||
outputExtensionPlayground,
|
||||
putReportTplIntoHTML(resultWithOutsource, true),
|
||||
);
|
||||
console.log(`HTML file generated successfully: ${outputExtensionPlayground}`);
|
||||
|
||||
// sidepanel.html
|
||||
writeFileSync(
|
||||
outputExtensionSidepanel,
|
||||
putReportTplIntoHTML(extensionSidepanelTpl, true),
|
||||
);
|
||||
console.log(`HTML file generated successfully: ${outputExtensionSidepanel}`);
|
||||
|
||||
// put the htmlElement.js into the extension
|
||||
safeCopyFile(
|
||||
join(__dirname, '../../web-integration/iife-script/htmlElement.js'),
|
||||
join(__dirname, '../unpacked-extension/lib/htmlElement.js'),
|
||||
);
|
||||
}
|
||||
async function zipDir(src: string, dest: string) {
|
||||
// console.log('cwd', dirname(src));
|
||||
execSync(`zip -r "${dest}" .`, {
|
||||
@ -167,25 +130,6 @@ async function zipDir(src: string, dest: string) {
|
||||
});
|
||||
}
|
||||
|
||||
async function packExtension() {
|
||||
const manifest = fileContentOfPath('../unpacked-extension/manifest.json');
|
||||
|
||||
const version = JSON.parse(manifest).version;
|
||||
const zipName = `midscene-extension-v${version}.zip`;
|
||||
const distFile = join(outputExtensionZipDir, zipName);
|
||||
ensureDirectoryExistence(distFile);
|
||||
|
||||
// zip the extension
|
||||
if (platform() !== 'win32') {
|
||||
await zipDir(outputExtensionUnpackedBaseDir, distFile);
|
||||
// print size of the zip file
|
||||
const size = statSync(distFile).size;
|
||||
console.log(`Zip file size: ${size} bytes`);
|
||||
} else {
|
||||
console.warn('zip is not supported on this platform, will skip it');
|
||||
}
|
||||
}
|
||||
|
||||
/* build task: report and demo pages*/
|
||||
function buildReport() {
|
||||
const reportHTMLContent = reportHTMLWithDump();
|
||||
@ -223,5 +167,3 @@ function buildReport() {
|
||||
}
|
||||
|
||||
buildReport();
|
||||
buildExtension();
|
||||
packExtension();
|
||||
|
||||
19
packages/visualizer/src/extension.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
export { default as Logo } from './component/logo';
|
||||
export {
|
||||
Playground,
|
||||
extensionAgentForTab,
|
||||
} from './component/playground-component';
|
||||
export { globalThemeConfig } from './component/color';
|
||||
export { useEnvConfig } from './component/store';
|
||||
|
||||
export {
|
||||
type WorkerRequestGetContext,
|
||||
type WorkerRequestSaveContext,
|
||||
type WorkerResponseGetContext,
|
||||
type WorkerResponseSaveContext,
|
||||
workerMessageTypes,
|
||||
getExtensionVersion,
|
||||
getTabInfo,
|
||||
currentWindowId,
|
||||
sendToWorker,
|
||||
} from './extension/utils';
|
||||
@ -47,7 +47,7 @@ export function getPlaygroundUrl(cacheContextId: string) {
|
||||
|
||||
export async function activeTab(): Promise<chrome.tabs.Tab> {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
chrome.tabs?.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
if (tabs?.[0]) {
|
||||
resolve(tabs[0]);
|
||||
} else {
|
||||
@ -70,7 +70,7 @@ export async function currentWindowId(): Promise<number> {
|
||||
}
|
||||
|
||||
export function getExtensionVersion() {
|
||||
return chrome.runtime.getManifest().version;
|
||||
return chrome.runtime?.getManifest()?.version || 'unknown';
|
||||
}
|
||||
|
||||
export async function getTabInfo(tabId: number) {
|
||||
|
||||
@ -11,19 +11,58 @@
|
||||
"midscene-playground": "./bin/midscene-playground"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/lib/index.js",
|
||||
"./bridge-mode": "./dist/lib/bridge-mode.js",
|
||||
"./bridge-mode-browser": "./dist/lib/bridge-mode-browser.js",
|
||||
"./utils": "./dist/lib/utils.js",
|
||||
"./ui-utils": "./dist/lib/ui-utils.js",
|
||||
"./puppeteer": "./dist/lib/puppeteer.js",
|
||||
"./playwright": "./dist/lib/playwright.js",
|
||||
"./playwright-report": "./dist/lib/playwright-report.js",
|
||||
"./playground": "./dist/lib/playground.js",
|
||||
"./midscene-playground": "./dist/lib/midscene-playground.js",
|
||||
"./appium": "./dist/lib/appium.js",
|
||||
"./chrome-extension": "./dist/lib/chrome-extension.js",
|
||||
"./yaml": "./dist/lib/yaml.js"
|
||||
".": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/lib/index.js"
|
||||
},
|
||||
"./bridge-mode": {
|
||||
"types": "./dist/types/bridge-mode.d.ts",
|
||||
"default": "./dist/lib/bridge-mode.js"
|
||||
},
|
||||
"./bridge-mode-browser": {
|
||||
"types": "./dist/types/bridge-mode-browser.d.ts",
|
||||
"default": "./dist/lib/bridge-mode-browser.js"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./dist/types/utils.d.ts",
|
||||
"default": "./dist/lib/utils.js"
|
||||
},
|
||||
"./ui-utils": {
|
||||
"types": "./dist/types/ui-utils.d.ts",
|
||||
"default": "./dist/lib/ui-utils.js"
|
||||
},
|
||||
"./puppeteer": {
|
||||
"types": "./dist/types/puppeteer.d.ts",
|
||||
"default": "./dist/lib/puppeteer.js"
|
||||
},
|
||||
"./playwright": {
|
||||
"types": "./dist/types/playwright.d.ts",
|
||||
"default": "./dist/lib/playwright.js"
|
||||
},
|
||||
"./playwright-report": {
|
||||
"types": "./dist/types/playwright-report.d.ts",
|
||||
"default": "./dist/lib/playwright-report.js"
|
||||
},
|
||||
"./playground": {
|
||||
"types": "./dist/types/playground.d.ts",
|
||||
"default": "./dist/lib/playground.js"
|
||||
},
|
||||
"./midscene-playground": {
|
||||
"types": "./dist/types/midscene-playground.d.ts",
|
||||
"default": "./dist/lib/midscene-playground.js"
|
||||
},
|
||||
"./appium": {
|
||||
"types": "./dist/types/appium.d.ts",
|
||||
"default": "./dist/lib/appium.js"
|
||||
},
|
||||
"./chrome-extension": {
|
||||
"types": "./dist/types/chrome-extension.d.ts",
|
||||
"default": "./dist/lib/chrome-extension.js"
|
||||
},
|
||||
"./yaml": {
|
||||
"types": "./dist/types/yaml.d.ts",
|
||||
"default": "./dist/lib/yaml.js"
|
||||
}
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
@ -96,7 +135,7 @@
|
||||
"@wdio/types": "9.0.4",
|
||||
"playwright": "1.44.1",
|
||||
"puppeteer": "24.2.0",
|
||||
"typescript": "~5.0.4",
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "3.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@ -5,7 +5,7 @@ import { ifInBrowser } from '@midscene/shared/utils';
|
||||
// extract html element from page
|
||||
let scriptFileContentCache: string | null = null;
|
||||
export const getHtmlElementScript = async () => {
|
||||
const scriptFileToRetrieve = chrome.runtime.getURL('lib/htmlElement.js');
|
||||
const scriptFileToRetrieve = chrome.runtime.getURL('scripts/htmlElement.js');
|
||||
if (scriptFileContentCache) return scriptFileContentCache;
|
||||
if (ifInBrowser) {
|
||||
const script = await fetch(scriptFileToRetrieve);
|
||||
@ -18,8 +18,9 @@ export const getHtmlElementScript = async () => {
|
||||
// inject water flow animation
|
||||
let waterFlowScriptFileContentCache: string | null = null;
|
||||
export const injectWaterFlowAnimation = async () => {
|
||||
const waterFlowScriptFileToRetrieve =
|
||||
chrome.runtime.getURL('lib/water-flow.js');
|
||||
const waterFlowScriptFileToRetrieve = chrome.runtime.getURL(
|
||||
'scripts/water-flow.js',
|
||||
);
|
||||
if (waterFlowScriptFileContentCache) return waterFlowScriptFileContentCache;
|
||||
if (ifInBrowser) {
|
||||
const script = await fetch(waterFlowScriptFileToRetrieve);
|
||||
@ -33,7 +34,7 @@ export const injectWaterFlowAnimation = async () => {
|
||||
let stopWaterFlowScriptFileContentCache: string | null = null;
|
||||
export const injectStopWaterFlowAnimation = async () => {
|
||||
const stopWaterFlowScriptFileToRetrieve = chrome.runtime.getURL(
|
||||
'lib/stop-water-flow.js',
|
||||
'scripts/stop-water-flow.js',
|
||||
);
|
||||
if (stopWaterFlowScriptFileContentCache)
|
||||
return stopWaterFlowScriptFileContentCache;
|
||||
|
||||
924
pnpm-lock.yaml
generated
@ -1,5 +1,6 @@
|
||||
packages:
|
||||
- apps/site
|
||||
- apps/chrome-extension
|
||||
- packages/cli
|
||||
- packages/shared
|
||||
- packages/core
|
||||
|
||||
@ -4,7 +4,9 @@ const semver = require('semver');
|
||||
const dayjs = require('dayjs');
|
||||
const args = require('minimist')(process.argv.slice(2));
|
||||
const bumpPrompt = require('@jsdevtools/version-bump-prompt');
|
||||
const { execa } = require('@esm2cjs/execa');
|
||||
const {
|
||||
execa
|
||||
} = require('@esm2cjs/execa');
|
||||
const chalk = require('chalk');
|
||||
|
||||
const step = (msg) => {
|
||||
@ -33,8 +35,7 @@ const run = async (bin, args, opts = {}) => {
|
||||
|
||||
const currentVersion = require('../package.json').version;
|
||||
|
||||
const actionPublishCanary =
|
||||
['preminor', 'prepatch'].includes(args.version) && process.env.CI;
|
||||
const actionPublishCanary = ['preminor', 'prepatch'].includes(args.version) && process.env.CI;
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
@ -58,7 +59,9 @@ async function main() {
|
||||
step('\nLinting all packages...');
|
||||
await lint();
|
||||
|
||||
const { stdout } = await run('git', ['diff'], {
|
||||
const {
|
||||
stdout
|
||||
} = await run('git', ['diff'], {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
if (stdout) {
|
||||
@ -75,7 +78,7 @@ async function main() {
|
||||
'--global',
|
||||
'user.email',
|
||||
process.env.GIT_USER_EMAIL ||
|
||||
'github-actions[bot]@users.noreply.github.com',
|
||||
'github-actions[bot]@users.noreply.github.com',
|
||||
]);
|
||||
}
|
||||
step('\nCommitting changes...');
|
||||
@ -145,7 +148,7 @@ async function test() {
|
||||
async function bumpExtensionVersion(newNpmVersion) {
|
||||
const manifestPath = path.join(
|
||||
__dirname,
|
||||
'../packages/visualizer/unpacked-extension/manifest.json',
|
||||
'../apps/chrome-extension/static/manifest.json',
|
||||
);
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
const [a, b] = manifest.version.split('.').map(Number);
|
||||
@ -256,4 +259,4 @@ async function cleanup() {
|
||||
main().catch((err) => {
|
||||
console.error(chalk.red(`Unexpected error: ${err.message}`));
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||