From bd09592e0b3465cd53a7bfef9a59341652421f3c Mon Sep 17 00:00:00 2001 From: arch3rPro Date: Sat, 30 Aug 2025 19:32:42 +0800 Subject: [PATCH] feat: add app playwright --- apps/dify/1.8.0/docker-compose.yml | 4 +- .../1.55.0-noble/config/package.json | 17 + .../1.55.0-noble/config/playwright.config.ts | 29 ++ apps/playwright/1.55.0-noble/config/server.js | 176 ++++++++++ apps/playwright/1.55.0-noble/config/start.sh | 18 + apps/playwright/1.55.0-noble/data.yml | 17 + .../1.55.0-noble/docker-compose.yml | 51 +++ apps/playwright/README.md | 313 ++++++++++++++++++ apps/playwright/README_en.md | 313 ++++++++++++++++++ apps/playwright/data.yml | 24 ++ apps/playwright/logo.png | Bin 0 -> 15328 bytes 11 files changed, 960 insertions(+), 2 deletions(-) create mode 100644 apps/playwright/1.55.0-noble/config/package.json create mode 100644 apps/playwright/1.55.0-noble/config/playwright.config.ts create mode 100644 apps/playwright/1.55.0-noble/config/server.js create mode 100755 apps/playwright/1.55.0-noble/config/start.sh create mode 100644 apps/playwright/1.55.0-noble/data.yml create mode 100644 apps/playwright/1.55.0-noble/docker-compose.yml create mode 100644 apps/playwright/README.md create mode 100644 apps/playwright/README_en.md create mode 100644 apps/playwright/data.yml create mode 100644 apps/playwright/logo.png diff --git a/apps/dify/1.8.0/docker-compose.yml b/apps/dify/1.8.0/docker-compose.yml index 7da0e86..66c9f84 100644 --- a/apps/dify/1.8.0/docker-compose.yml +++ b/apps/dify/1.8.0/docker-compose.yml @@ -1041,7 +1041,7 @@ services: - ssrf_proxy_network - default worker: - image: langgenius/dify-api:1.7.1 + image: langgenius/dify-api:1.8.0 env_file: - dify.env restart: always @@ -1570,7 +1570,7 @@ services: - ssrf_proxy_network - default web: - image: langgenius/dify-web:1.7.1 + image: langgenius/dify-web:1.8.0 container_name: ${CONTAINER_NAME} env_file: - dify.env diff --git a/apps/playwright/1.55.0-noble/config/package.json b/apps/playwright/1.55.0-noble/config/package.json new file mode 100644 index 0000000..38fb17a --- /dev/null +++ b/apps/playwright/1.55.0-noble/config/package.json @@ -0,0 +1,17 @@ +{ + "name": "playwright-test-environment", + "version": "1.0.0", + "description": "Playwright test environment for 1Panel", + "main": "server.js", + "scripts": { + "start": "node server.js", + "test": "playwright test" + }, + "dependencies": { + "@playwright/test": "^1.55.0", + "express": "^4.18.2" + }, + "keywords": ["playwright", "testing", "automation"], + "author": "1Panel-Appstore", + "license": "MIT" +} \ No newline at end of file diff --git a/apps/playwright/1.55.0-noble/config/playwright.config.ts b/apps/playwright/1.55.0-noble/config/playwright.config.ts new file mode 100644 index 0000000..2ffb1f3 --- /dev/null +++ b/apps/playwright/1.55.0-noble/config/playwright.config.ts @@ -0,0 +1,29 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [['html', { outputFolder: './test-results' }]], + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], +}); \ No newline at end of file diff --git a/apps/playwright/1.55.0-noble/config/server.js b/apps/playwright/1.55.0-noble/config/server.js new file mode 100644 index 0000000..1f5114f --- /dev/null +++ b/apps/playwright/1.55.0-noble/config/server.js @@ -0,0 +1,176 @@ +const express = require('express'); +const path = require('path'); +const fs = require('fs'); +const { spawn } = require('child_process'); + +const app = express(); +const port = process.env.PORT || 3000; + +app.use(express.json()); +app.use('/test-results', express.static(path.join(__dirname, 'test-results'))); +app.use('/tests', express.static(path.join(__dirname, 'tests'))); + +app.get('/', (req, res) => { + res.send(` + + + + Playwright Test Environment + + + +
+

🎭 Playwright Test Environment

+

✅ Environment is ready!

+ +
+

Quick Actions

+ + View Reports + +
+ +
+

Environment Info

+

Playwright Version: v1.55.0

+

Available Browsers: Chromium, Firefox, WebKit

+

Test Directory: ./tests

+

Reports Directory: ./test-results

+
+ +
+

Usage Instructions

+
    +
  1. Upload test files to the tests/ directory
  2. +
  3. Configure tests in playwright.config.ts
  4. +
  5. Run tests using the button above or CLI command
  6. +
  7. View HTML reports in the test-results directory
  8. +
+ +

Example Test Code

+
+import { test, expect } from '@playwright/test'; + +test('basic test', async ({ page }) => { + await page.goto('https://playwright.dev/'); + await expect(page).toHaveTitle(/Playwright/); +}); +
+
+
+ + + + + `); +}); + +app.post('/api/run-tests', (req, res) => { + const testProcess = spawn('npx', ['playwright', 'test'], { + cwd: process.cwd(), + stdio: 'inherit' + }); + + res.json({ message: 'Tests execution started, check reports in 30 seconds' }); +}); + +app.post('/api/generate-example', (req, res) => { + const exampleTest = `import { test, expect } from '@playwright/test'; + +test('playwright homepage test', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); + + // create a locator + const getStarted = page.getByRole('link', { name: 'Get started' }); + + // Expect an attribute "to be strictly equal" to the value. + await expect(getStarted).toHaveAttribute('href', '/docs/intro'); + + // Click the get started link. + await getStarted.click(); + + // Expects the URL to contain intro. + await expect(page).toHaveURL(/.*intro/); +}); + +test('example.com test', async ({ page }) => { + await page.goto('https://example.com'); + await expect(page).toHaveTitle('Example Domain'); + await expect(page.locator('h1')).toContainText('Example Domain'); +});`; + + if (!fs.existsSync('./tests')) { + fs.mkdirSync('./tests', { recursive: true }); + } + + fs.writeFileSync('./tests/example.spec.ts', exampleTest); + res.json({ message: 'Example test generated at ./tests/example.spec.ts' }); +}); + +app.listen(port, '0.0.0.0', () => { + console.log(`🎭 Playwright server running at http://0.0.0.0:${port}`); + console.log(`📊 Test reports will be available at http://0.0.0.0:${port}/test-results`); +}); \ No newline at end of file diff --git a/apps/playwright/1.55.0-noble/config/start.sh b/apps/playwright/1.55.0-noble/config/start.sh new file mode 100755 index 0000000..ba90a01 --- /dev/null +++ b/apps/playwright/1.55.0-noble/config/start.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +echo "🎭 Setting up Playwright Test Environment..." + +# 确保目录存在 +mkdir -p tests test-results + +# 安装依赖 +echo "📦 Installing dependencies..." +npm install + +# 安装 Playwright 浏览器 +echo "🌐 Installing Playwright browsers..." +npx playwright install + +# 启动服务器 +echo "🚀 Starting Playwright server..." +node server.js \ No newline at end of file diff --git a/apps/playwright/1.55.0-noble/data.yml b/apps/playwright/1.55.0-noble/data.yml new file mode 100644 index 0000000..2ade09f --- /dev/null +++ b/apps/playwright/1.55.0-noble/data.yml @@ -0,0 +1,17 @@ +additionalProperties: + formFields: + - default: 3000 + edit: true + envKey: PANEL_APP_PORT_HTTP + label: + en: Port + zh: 端口 + zh-Hant: 埠 + ja: ポート + ms: Port + pt-br: Porta + ru: Порт + ko: 포트 + required: true + rule: paramPort + type: number \ No newline at end of file diff --git a/apps/playwright/1.55.0-noble/docker-compose.yml b/apps/playwright/1.55.0-noble/docker-compose.yml new file mode 100644 index 0000000..4007178 --- /dev/null +++ b/apps/playwright/1.55.0-noble/docker-compose.yml @@ -0,0 +1,51 @@ +services: + playwright-init: + image: mcr.microsoft.com/playwright:v1.55.0-noble + user: "root" + volumes: + - ./data:/home/pwuser/app + - ./config:/home/pwuser/config:ro + working_dir: /home/pwuser/app + command: > + sh -c " + cp -r /home/pwuser/config/* /home/pwuser/app/ && + chown -R pwuser:pwuser /home/pwuser/app && + chmod +x /home/pwuser/app/start.sh + " + restart: "no" + + playwright: + image: mcr.microsoft.com/playwright:v1.55.0-noble + container_name: ${CONTAINER_NAME} + restart: always + networks: + - 1panel-network + ports: + - "${PANEL_APP_PORT_HTTP}:3000" + volumes: + - ./data:/home/pwuser/app + working_dir: /home/pwuser/app + user: "pwuser" + environment: + - NODE_ENV=production + - PORT=3000 + init: true + ipc: host + security_opt: + - seccomp:unconfined + command: "./start.sh" + depends_on: + playwright-init: + condition: service_completed_successfully + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 90s + labels: + createdBy: "Apps" + +networks: + 1panel-network: + external: true \ No newline at end of file diff --git a/apps/playwright/README.md b/apps/playwright/README.md new file mode 100644 index 0000000..1626c7f --- /dev/null +++ b/apps/playwright/README.md @@ -0,0 +1,313 @@ +# Playwright + +现代 Web 应用自动化测试环境,基于 Microsoft Playwright 构建。支持 Chromium、Firefox 和 WebKit 浏览器,提供完整的端到端测试解决方案。 + +![](https://cdn.jsdelivr.net/gh/xiaoY233/PicList@main/public/assets/Playwright.png) + +![](https://img.shields.io/badge/Copyright-arch3rPro-ff9800?style=flat&logo=github&logoColor=white) + +## ✨ 核心特性 + +- **🚀 快速可靠**:使用 Microsoft 官方 Playwright v1.55.0 镜像 +- **🌐 多浏览器支持**:支持 Chromium、Firefox 和 WebKit +- **🎯 精确测试**:Web-first 断言,自动等待和重试 +- **📊 丰富报告**:HTML 测试报告,支持跟踪和截图 +- **🔧 高度可配置**:灵活的测试配置和环境设置 +- **🛡️ 安全稳定**:遵循官方安全最佳实践 + +## 📋 系统要求 + +- Docker 环境 +- 1Panel 控制面板 +- 至少 2GB 可用内存 + +## 🚀 快速开始 + +### 1Panel 部署 + +1. 在 1Panel 应用商店中搜索 "Playwright" +2. 点击安装并配置以下参数: + - **端口**:Web 服务访问端口(默认:3000) + - **浏览器类型**:选择默认浏览器引擎 + - **无头模式**:是否启用无头模式运行 + - **视窗尺寸**:浏览器视窗的宽度和高度 + - **用户代理**:自定义浏览器用户代理字符串 + - **用户/组 ID**:容器运行的用户和组标识符 + +3. 点击确认安装 + +### 访问服务 + +安装完成后,您可以通过以下方式访问: + +- **管理界面**:`http://your-server-ip:port/` +- **测试报告**:`http://your-server-ip:port/test-results/` +- **测试文件**:`http://your-server-ip:port/tests/` + +## ⚙️ 配置说明 + +### 浏览器类型 + +- **Chromium**:Google Chrome 内核,推荐选择 +- **Firefox**:Mozilla Firefox 引擎 +- **WebKit**:Safari 内核,适合移动端测试 + +### 运行模式 + +- **有头模式**:显示浏览器界面,适合调试和开发 +- **无头模式**:后台运行,适合 CI/CD 和生产环境 + +### 安全配置 + +- **用户 ID**:建议使用非 root 用户(默认:1000) +- **组 ID**:对应的用户组(默认:1000) +- 应用自动配置安全沙箱和权限 + +## 🧪 使用指南 + +### 创建测试 + +1. 访问管理界面 +2. 点击 "Generate Example Test" 生成示例测试 +3. 编辑 `/app/tests/` 目录下的测试文件 +4. 使用 TypeScript 或 JavaScript 编写测试 + +### 示例测试代码 + +```typescript +import { test, expect } from '@playwright/test'; + +test('basic test', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // 检查页面标题 + await expect(page).toHaveTitle(/Playwright/); + + // 点击链接 + const getStarted = page.getByRole('link', { name: 'Get started' }); + await getStarted.click(); + + // 验证URL变化 + await expect(page).toHaveURL(/.*intro/); +}); +``` + +### 运行测试 + +1. 在管理界面点击 "Run All Tests" +2. 或使用命令行:`npx playwright test` +3. 查看生成的 HTML 报告 + +### 配置文件 + +应用会自动生成 `playwright.config.ts` 配置文件: + +```typescript +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + retries: 2, + reporter: [['html', { outputFolder: './test-results' }]], + use: { + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { name: 'chromium', use: devices['Desktop Chrome'] }, + { name: 'firefox', use: devices['Desktop Firefox'] }, + { name: 'webkit', use: devices['Desktop Safari'] }, + ], +}); +``` + +## 📁 目录结构 + +``` +/app/ +├── tests/ # 测试文件目录 +├── test-results/ # 测试报告和结果 +├── config/ # 配置文件 +├── projects/ # 项目文件 +├── playwright.config.ts # Playwright 配置 +├── package.json # Node.js 依赖 +└── server.js # Web 管理界面 +``` + +## 🔧 高级功能 + +### 并行测试 + +- 支持多浏览器并行执行 +- 自动分配测试工作负载 +- CI/CD 环境优化 + +### 测试跟踪 + +- 自动生成测试执行跟踪 +- 可视化测试步骤和时间线 +- 失败时自动截图 + +### 自定义配置 + +- 支持自定义 Playwright 配置 +- 灵活的项目设置 +- 环境变量配置 + +### 🔄 自定义脚本执行 + +应用采用了配置文件分离设计,您可以轻松替换和自定义执行脚本: + +#### 配置文件位置 + +所有应用配置文件位于 `config/` 目录: + +``` +config/ +├── package.json # Node.js 依赖配置 +├── playwright.config.ts # Playwright 测试配置 +├── server.js # Web 管理界面服务器 +└── start.sh # 应用启动脚本 +``` + +#### 替换自定义脚本 + +1. **替换启动脚本** + ```bash + # 编辑启动脚本 + vi config/start.sh + ``` + +2. **替换 Web 服务器** + ```bash + # 编辑或替换服务器脚本 + vi config/server.js + ``` + +3. **添加自定义脚本** + ```bash + # 在 config 目录添加自定义脚本 + vi config/my-custom-script.js + ``` + +4. **修改启动逻辑** + + 编辑 `config/start.sh` 来调用您的自定义脚本: + + ```bash + #!/bin/bash + + echo "🎭 Setting up Playwright Test Environment..." + + # 确保目录存在 + mkdir -p tests test-results + + # 安装依赖 + echo "📦 Installing dependencies..." + npm install + + # 安装 Playwright 浏览器 + echo "🌐 Installing Playwright browsers..." + npx playwright install + + # 运行您的自定义脚本 + echo "🔧 Running custom script..." + node my-custom-script.js + + # 启动服务器(可选) + echo "🚀 Starting Playwright server..." + node server.js + ``` + +#### 应用更改 + +修改配置文件后,重启容器以应用更改: + +```bash +# 在 1Panel 中重启应用 +# 或使用 Docker Compose 命令 +docker-compose restart playwright +``` + +#### 自定义脚本示例 + +创建一个简单的自定义测试脚本 `config/my-test.js`: + +```javascript +const { chromium } = require('playwright'); + +(async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + + await page.goto('https://example.com'); + console.log('页面标题:', await page.title()); + + await browser.close(); +})(); +``` + +然后在 `start.sh` 中调用: + +```bash +# 运行自定义测试 +echo "🧪 Running custom test..." +node my-test.js +``` + +#### 注意事项 + +- 所有自定义脚本必须放在 `config/` 目录中 +- 修改后需要重启容器才能生效 +- 保持脚本文件的可执行权限 +- 遵循 Playwright 安全最佳实践 + +## 🐛 故障排除 + +### 常见问题 + +1. **容器启动失败** + - 检查内存是否充足(至少2GB) + - 确认端口未被占用 + - 查看容器日志 + +2. **测试运行失败** + - 检查浏览器配置 + - 验证测试文件语法 + - 确认网络连接 + +3. **权限问题** + - 调整用户ID和组ID配置 + - 检查文件系统权限 + - 确认数据卷挂载 + +### 日志查看 + +```bash +# 查看容器日志 +docker logs playwright + +# 实时跟踪日志 +docker logs -f playwright +``` + +## 📚 学习资源 + +- [Playwright 官方文档](https://playwright.dev/docs/intro) +- [测试最佳实践](https://playwright.dev/docs/best-practices) +- [API 参考](https://playwright.dev/docs/api/class-playwright) +- [示例和模式](https://playwright.dev/docs/test-runners) + +## 🔗 相关链接 + +- [官方网站](https://playwright.dev/) +- [GitHub 仓库](https://github.com/microsoft/playwright) +- [社区讨论](https://github.com/microsoft/playwright/discussions) +- [1Panel 文档](https://1panel.cn/docs/) + +## 📄 许可证 + +本项目遵循 Playwright 的开源许可证。详细信息请参考 [官方仓库](https://github.com/microsoft/playwright)。 + +## 🤝 贡献 + +欢迎提交 Issue 和 Pull Request 来帮助改进这个应用配置。 \ No newline at end of file diff --git a/apps/playwright/README_en.md b/apps/playwright/README_en.md new file mode 100644 index 0000000..700773d --- /dev/null +++ b/apps/playwright/README_en.md @@ -0,0 +1,313 @@ +# Playwright + +Modern web application automation testing environment built with Microsoft Playwright. Supports Chromium, Firefox, and WebKit browsers, providing a complete end-to-end testing solution. + +![](https://cdn.jsdelivr.net/gh/xiaoY233/PicList@main/public/assets/Playwright.png) + +![](https://img.shields.io/badge/Copyright-arch3rPro-ff9800?style=flat&logo=github&logoColor=white) + +## ✨ Key Features + +- **🚀 Fast and Reliable**: Uses Microsoft official Playwright v1.55.0 image +- **🌐 Multi-Browser Support**: Supports Chromium, Firefox, and WebKit +- **🎯 Precise Testing**: Web-first assertions with auto-waiting and retry +- **📊 Rich Reports**: HTML test reports with tracing and screenshots +- **🔧 Highly Configurable**: Flexible test configuration and environment settings +- **🛡️ Secure and Stable**: Follows official security best practices + +## 📋 System Requirements + +- Docker environment +- 1Panel control panel +- At least 2GB available memory + +## 🚀 Quick Start + +### 1Panel Deployment + +1. Search for "Playwright" in the 1Panel app store +2. Click install and configure the following parameters: + - **Port**: Web service access port (default: 3000) + - **Browser Type**: Choose default browser engine + - **Headless Mode**: Whether to enable headless mode + - **Viewport Size**: Browser viewport width and height + - **User Agent**: Custom browser user agent string + - **User/Group ID**: Container runtime user and group identifiers + +3. Click confirm to install + +### Accessing the Service + +After installation, you can access via: + +- **Management Interface**: `http://your-server-ip:port/` +- **Test Reports**: `http://your-server-ip:port/test-results/` +- **Test Files**: `http://your-server-ip:port/tests/` + +## ⚙️ Configuration Guide + +### Browser Types + +- **Chromium**: Google Chrome kernel, recommended choice +- **Firefox**: Mozilla Firefox engine +- **WebKit**: Safari kernel, suitable for mobile testing + +### Running Modes + +- **Headed Mode**: Shows browser interface, suitable for debugging and development +- **Headless Mode**: Runs in background, suitable for CI/CD and production environments + +### Security Configuration + +- **User ID**: Recommended to use non-root user (default: 1000) +- **Group ID**: Corresponding user group (default: 1000) +- Application automatically configures security sandbox and permissions + +## 🧪 Usage Guide + +### Creating Tests + +1. Access the management interface +2. Click "Generate Example Test" to generate sample tests +3. Edit test files in the `/app/tests/` directory +4. Write tests using TypeScript or JavaScript + +### Sample Test Code + +```typescript +import { test, expect } from '@playwright/test'; + +test('basic test', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Check page title + await expect(page).toHaveTitle(/Playwright/); + + // Click link + const getStarted = page.getByRole('link', { name: 'Get started' }); + await getStarted.click(); + + // Verify URL change + await expect(page).toHaveURL(/.*intro/); +}); +``` + +### Running Tests + +1. Click "Run All Tests" in the management interface +2. Or use command line: `npx playwright test` +3. View the generated HTML report + +### Configuration File + +The application automatically generates `playwright.config.ts` configuration file: + +```typescript +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + retries: 2, + reporter: [['html', { outputFolder: './test-results' }]], + use: { + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { name: 'chromium', use: devices['Desktop Chrome'] }, + { name: 'firefox', use: devices['Desktop Firefox'] }, + { name: 'webkit', use: devices['Desktop Safari'] }, + ], +}); +``` + +## 📁 Directory Structure + +``` +/app/ +├── tests/ # Test files directory +├── test-results/ # Test reports and results +├── config/ # Configuration files +├── projects/ # Project files +├── playwright.config.ts # Playwright configuration +├── package.json # Node.js dependencies +└── server.js # Web management interface +``` + +## 🔧 Advanced Features + +### Parallel Testing + +- Supports multi-browser parallel execution +- Automatic test workload distribution +- CI/CD environment optimization + +### Test Tracing + +- Automatically generates test execution traces +- Visualizes test steps and timeline +- Automatic screenshots on failure + +### Custom Configuration + +- Supports custom Playwright configuration +- Flexible project settings +- Environment variable configuration + +### 🔄 Custom Script Execution + +The application uses a separated configuration file design, allowing you to easily replace and customize execution scripts: + +#### Configuration File Location + +All application configuration files are located in the `config/` directory: + +``` +config/ +├── package.json # Node.js dependency configuration +├── playwright.config.ts # Playwright test configuration +├── server.js # Web management interface server +└── start.sh # Application startup script +``` + +#### Replacing Custom Scripts + +1. **Replace Startup Script** + ```bash + # Edit startup script + vi config/start.sh + ``` + +2. **Replace Web Server** + ```bash + # Edit or replace server script + vi config/server.js + ``` + +3. **Add Custom Scripts** + ```bash + # Add custom scripts in config directory + vi config/my-custom-script.js + ``` + +4. **Modify Startup Logic** + + Edit `config/start.sh` to call your custom scripts: + + ```bash + #!/bin/bash + + echo "🎭 Setting up Playwright Test Environment..." + + # Ensure directories exist + mkdir -p tests test-results + + # Install dependencies + echo "📦 Installing dependencies..." + npm install + + # Install Playwright browsers + echo "🌐 Installing Playwright browsers..." + npx playwright install + + # Run your custom script + echo "🔧 Running custom script..." + node my-custom-script.js + + # Start server (optional) + echo "🚀 Starting Playwright server..." + node server.js + ``` + +#### Applying Changes + +After modifying configuration files, restart the container to apply changes: + +```bash +# Restart application in 1Panel +# Or use Docker Compose command +docker-compose restart playwright +``` + +#### Custom Script Example + +Create a simple custom test script `config/my-test.js`: + +```javascript +const { chromium } = require('playwright'); + +(async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + + await page.goto('https://example.com'); + console.log('Page Title:', await page.title()); + + await browser.close(); +})(); +``` + +Then call it in `start.sh`: + +```bash +# Run custom test +echo "🧪 Running custom test..." +node my-test.js +``` + +#### Notes + +- All custom scripts must be placed in the `config/` directory +- Container restart is required after modifications +- Maintain executable permissions for script files +- Follow Playwright security best practices + +## 🐛 Troubleshooting + +### Common Issues + +1. **Container Startup Failure** + - Check if memory is sufficient (at least 2GB) + - Confirm port is not in use + - View container logs + +2. **Test Execution Failure** + - Check browser configuration + - Verify test file syntax + - Confirm network connection + +3. **Permission Issues** + - Adjust user ID and group ID configuration + - Check file system permissions + - Confirm data volume mounting + +### Log Viewing + +```bash +# View container logs +docker logs playwright + +# Follow logs in real-time +docker logs -f playwright +``` + +## 📚 Learning Resources + +- [Playwright Official Documentation](https://playwright.dev/docs/intro) +- [Testing Best Practices](https://playwright.dev/docs/best-practices) +- [API Reference](https://playwright.dev/docs/api/class-playwright) +- [Examples and Patterns](https://playwright.dev/docs/test-runners) + +## 🔗 Related Links + +- [Official Website](https://playwright.dev/) +- [GitHub Repository](https://github.com/microsoft/playwright) +- [Community Discussions](https://github.com/microsoft/playwright/discussions) +- [1Panel Documentation](https://1panel.cn/docs/) + +## 📄 License + +This project follows the open source license of Playwright. For details, please refer to the [official repository](https://github.com/microsoft/playwright). + +## 🤝 Contributing + +Welcome to submit Issues and Pull Requests to help improve this application configuration. \ No newline at end of file diff --git a/apps/playwright/data.yml b/apps/playwright/data.yml new file mode 100644 index 0000000..0c7cd74 --- /dev/null +++ b/apps/playwright/data.yml @@ -0,0 +1,24 @@ +name: Playwright +tags: + - 开发工具 + - 实用工具 +title: Playwright +description: 基于 Playwright 的自动化测试环境 +additionalProperties: + key: playwright + name: Playwright + tags: + - DevTool + - Tool + shortDescZh: 专业的 Web 应用自动化测试环境 + shortDescEn: Professional web application automation testing environment + description: + zh: 基于 Microsoft Playwright 的现代 Web 应用自动化测试环境。支持 Chromium、Firefox 和 WebKit 浏览器,提供完整的端到端测试解决方案。 + en: Modern web application automation testing environment based on Microsoft Playwright. Supports Chromium, Firefox, and WebKit browsers with comprehensive end-to-end testing solutions. + type: website + crossVersionUpdate: true + limit: 0 + recommend: 1 + website: https://playwright.dev/ + github: https://github.com/microsoft/playwright + document: https://playwright.dev/docs/intro \ No newline at end of file diff --git a/apps/playwright/logo.png b/apps/playwright/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..74558c6d56ff46418d61a4bc6fa0e846c07f1a3a GIT binary patch literal 15328 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 Qc=7O*