Commit eaf6ca4f authored by wushanjing's avatar wushanjing

Initial test environment version

parents
Pipeline #1279 canceled with stages
# E2E Runner Agent
Use this agent profile when the user asks to run, diagnose, extend, or summarize SaaS E2E tests in this repository.
## Operating Loop
1. Inspect the requested profile with `scripts/agent_e2e.py plan <profile>` or the `saas-e2e.build_plan` MCP tool.
2. Check `runtime/e2e_accounts.json` before chain tests; prefer existing ledger state over creating fresh accounts.
3. Run the narrowest useful profile first:
- `seed` for account/data creation.
- `standalone` for entry-point smoke coverage.
- `chain` for ledger-dependent progression.
- `main` for the full closed loop.
4. On failure, read the newest bug report from `bugs/`, inspect screenshots, and only then patch selectors or test flow.
5. Keep generated reports under `reports/` and runtime state under `runtime/`.
## Commands
```powershell
.\.venv\Scripts\python.exe .\scripts\agent_e2e.py plan standalone
.\.venv\Scripts\python.exe .\scripts\agent_e2e.py run main
.\.venv\Scripts\python.exe .\scripts\agent_e2e.py summary
```
{
"mcpServers": {
"saas-e2e": {
"command": ".\\.venv\\Scripts\\python.exe",
"args": [
".\\scripts\\mcp_saas_e2e.py"
],
"cwd": ".",
"env": {
"PYTHONIOENCODING": "utf-8"
}
}
}
}
---
name: saas-e2e-automation
description: SaaS E2E automation workflow for this repository. Use when Codex needs to run, plan, debug, extend, or summarize pytest + Playwright + Allure tests; inspect runtime/e2e_accounts.json; use the local saas-e2e MCP server; work with e2e/tests, e2e/pages, scripts/probe_*.py, bugs, or reports.
---
# SaaS E2E Automation
Use the repository's automation core instead of rediscovering test state manually.
## Quick Workflow
1. Inspect the target run:
```powershell
.\.venv\Scripts\python.exe .\scripts\agent_e2e.py plan standalone
```
2. Pick the smallest profile that exercises the requested behavior.
3. Run through the agent entry:
```powershell
.\.venv\Scripts\python.exe .\scripts\agent_e2e.py run main
```
4. On failure, inspect:
```powershell
.\.venv\Scripts\python.exe .\scripts\agent_e2e.py bugs
.\.venv\Scripts\python.exe .\scripts\agent_e2e.py summary
```
5. Patch Page Objects in `e2e/pages/` before patching tests when the problem is selector or navigation reuse.
## Profiles
- `seed`: data/account creation tests.
- `standalone`: tests that can run as entry points.
- `chain`: ledger-dependent progression tests.
- `main`: full closed-loop test.
- `saas-001`, `saas-003`, `saas-004`, `auth`, `push`, `order`: focused business areas.
## MCP
Project MCP config lives at `.codex/mcp/saas-e2e.json`. The server is `scripts/mcp_saas_e2e.py` and exposes:
- `list_tests`
- `inspect_accounts`
- `latest_bugs`
- `allure_summary`
- `build_plan`
- `run_pytest`
Prefer MCP tools when available. Use `scripts/agent_e2e.py` as the shell fallback.
## References
Read `references/test-matrix.md` when choosing test order, prerequisites, or ledger expectations.
display_name: SaaS E2E Automation
short_description: Plan, run, and debug this repo's Playwright E2E tests.
default_prompt: Use the SaaS E2E automation skill to inspect the test ledger, choose the right profile, run the narrowest useful pytest command, and summarize failures with bug and Allure artifacts.
# Test Matrix
Use this matrix to choose the narrowest useful run profile.
| Profile | Pytest target | Use for |
| --- | --- | --- |
| `seed` | `-m e2e_seed` | Create or refresh account data in `runtime/e2e_accounts.json`. |
| `standalone` | `-m e2e_standalone` | Smoke tests that can be launched directly. |
| `chain` | `-m e2e_chain` | Continue ledger-dependent flows after seed/opening steps exist. |
| `main` | `e2e/tests/组合大流程/test_e2e_main_001_full_closed_loop.py` | Full registration, opening, authorization, dispatch, order loop. |
| `saas-001` | `-m e2e_saas_001` | Enterprise registration and certification. |
| `saas-003` | `-m e2e_saas_003` | Wanyouji app opening. |
| `saas-004` | `-m e2e_saas_004` | Wanzhanggui app opening. |
| `auth` | `-m e2e_auth_closed_loop` | Authorization key generation, activation, and permissions. |
| `push` | `-m "e2e_push_001 or e2e_push_003"` | Workshop authorization and dispatch strategy. |
| `order` | `-m "e2e_order_001 or e2e_order_002"` | Manual order dispatch and revoke verification. |
Before running `chain`, `auth`, `push`, or `order`, inspect account state:
```powershell
.\.venv\Scripts\python.exe .\scripts\agent_e2e.py accounts
```
Use recent bug reports as the first failure artifact:
```powershell
.\.venv\Scripts\python.exe .\scripts\agent_e2e.py bugs --limit 3
```
TENANT_BASE_URL=https://test-lianlianyun.lianlianwz.com
TENANT_HOME_URL=https://test-lianlianyun.lianlianwz.com/home
ADMIN_URL=https://test-saas-adminui.lianlianwz.com/pages/home/index
ADMIN_USERNAME=wangss
ADMIN_PASSWORD=123456
E2E_REGISTER_PASSWORD=test123456
E2E_PHONE_PREFIX=19
E2E_MAX_REGISTER_ATTEMPTS=5
E2E_ENTERPRISE_NAME_PREFIX=E2E测试企业
E2E_HEADLESS=false
# 可选:manual / feishu / static
E2E_VERIFICATION_CODE_MODE=manual
E2E_VERIFICATION_CODE_STATIC=
E2E_IMAGE_CAPTCHA_MODE=ddddocr
FEISHU_APP_ID=
FEISHU_APP_SECRET=
FEISHU_CHAT_ID=
FEISHU_TIMEOUT_SECONDS=60
FEISHU_POLL_INTERVAL_SECONDS=3
.env
.venv/
.pytest_cache/
__pycache__/
*.py[cod]
runtime/
reports/
bugs/*.png
~$*
*.tmp
*.log
.DS_Store
Thumbs.db
# SaaS E2E 测试框架(测试环境版)
当前框架使用 Python 3.11 + pytest + Playwright,已先落地 `E2E-SAAS-001`
租户平台注册企业 -> SaaS 主站企业认证审核通过 -> 租户端验证认证状态。
## 环境准备
```powershell
py -3.11 -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
python -m playwright install chromium
Copy-Item .env.example .env
```
## 运行
```powershell
.\.venv\Scripts\Activate.ps1
pytest e2e/tests/注册登录权限/test_e2e_saas_001_enterprise_certification.py
```
手动短信验证码模式需要允许终端输入,建议使用:
```powershell
.\scripts\run_e2e_with_allure.ps1
```
测试执行后会生成 Allure 原始结果:
```text
reports/allure-results
```
如果本机已安装 Allure CLI,可生成 HTML 报告:
```powershell
allure generate reports/allure-results -o reports/allure-report --clean
allure open reports/allure-report
```
也可以直接使用项目脚本:
```powershell
.\scripts\run_e2e_with_allure.ps1
```
## 用例执行分层
当前用例分成三类:
- `e2e_seed`:种子用例,会注册或生成后续流程需要的账号数据。
- `e2e_standalone`:可作为入口单独运行;如果依赖台账,也有兜底测试账号。
- `e2e_chain`:链路推进用例,需要 `runtime/e2e_accounts.json` 中已有匹配状态的数据。
- `e2e_ledger`:会从 `runtime/e2e_accounts.json` 读取账号,并在成功后更新账号状态。
账号台账文件:
```text
runtime/e2e_accounts.json
```
## 用例执行矩阵
| 用例 | 类型 | 是否适合直接跑 | 前置数据 | 成功后更新 |
| --- | --- | --- | --- | --- |
| `E2E-SAAS-001` 注册商户并完成主站认证 | `e2e_seed`, `e2e_standalone` | 是 | 无。需要短信验证码/图形验证码能力 | 新增 `certification_status=approved` 账号 |
| `E2E-SAAS-003` 已认证商户开通丸友集 | `e2e_ledger`, `e2e_standalone` | 是 | 优先取 `approved + not_opened` 丸友集账号;无台账时用 `.env` 固定已认证账号 | 更新 `wanyouji_status=opened` |
| `E2E-SAAS-004` 已认证商户开通丸掌柜 | `e2e_ledger`, `e2e_standalone` | 单独准备丸掌柜账号时运行 | 优先取 `approved + not_opened` 丸掌柜账号;无台账时用 `.env` 固定已认证账号 | 更新 `wanzhanggui_status=opened` |
| `E2E-AUTH-001` 丸友集绑定默认丸掌柜并配置权限 | `e2e_ledger`, `e2e_chain` | 有数据才适合 | 需要 `wanyouji_status=opened` 且未绑定,或 `permission_pending` 账号 | 更新绑定状态为 `bound` |
| `E2E-PUSH-001` 丸掌柜新增丸友集工作室授权 | `e2e_ledger`, `e2e_chain` | 有数据才适合 | 需要丸友集已绑定目标丸掌柜,但还没有 `workshop_authorizations` | 更新 `workshop_authorizations` |
| `E2E-PUSH-003` 丸掌柜新增分单策略 | `e2e_ledger`, `e2e_chain` | 有数据才适合 | 需要已完成工作室授权。多工作室同时分发/多平台分发至少需要 2 个已授权工作室。策略类型通过 `E2E_PUSH_STRATEGY_TYPE` 指定 | 记录 `dispatch_strategies` |
| `E2E-MAIN-001` 主流程闭环 | `e2e_ledger`, `e2e_chain` | 是 | 固定使用丸掌柜账号 `19147509308`,依次执行除 `E2E-SAAS-004` 外的主链路 | 完成注册、开通、授权、配置、发单、接单验证 |
## 推荐执行入口
只生成基础账号:
```powershell
pytest -m e2e_seed --alluredir=reports/allure-results
```
只跑适合直接启动的用例:
```powershell
pytest -m e2e_standalone --alluredir=reports/allure-results
```
跑后续链路推进用例:
```powershell
pytest -m e2e_chain --alluredir=reports/allure-results
```
按当前完整链路顺序跑一次:
```powershell
pytest `
e2e/tests/注册登录权限/test_e2e_saas_001_enterprise_certification.py `
e2e/tests/应用开通授权/test_e2e_saas_003_wanyouji_app_opening.py `
e2e/tests/应用开通授权/test_e2e_auth_001_authorization_closed_loop.py `
e2e/tests/推单配置/test_e2e_push_001_wanzhanggui_workshop_authorization.py `
e2e/tests/推单配置/test_e2e_push_003_dispatch_strategy.py `
--alluredir=reports/allure-results
```
直接执行主流程闭环:
```powershell
pytest e2e/tests/组合大流程/test_e2e_main_001_full_closed_loop.py --alluredir=reports/allure-results
```
主流程默认策略类型为 `单平台指定`,可通过 `E2E_MAIN_STRATEGY_TYPE` 切换:
```powershell
$env:E2E_MAIN_STRATEGY_TYPE="多工作室同时分发"
pytest e2e/tests/组合大流程/test_e2e_main_001_full_closed_loop.py --alluredir=reports/allure-results
```
当前主链路默认使用已开通丸掌柜账号:
```text
19147509308 / test123456
```
`E2E-SAAS-004` 暂时作为独立的丸掌柜账号准备用例,不放入默认主链路。需要新丸掌柜账号时再单独执行:
```powershell
pytest -m e2e_saas_004 --alluredir=reports/allure-results
```
单独跑某条链路用例时,如果前置数据不足,脚本会 `skip` 并提示缺少哪类台账状态。此时先跑上游用例补数据即可。
`E2E-PUSH-003` 默认策略类型为 `单平台指定`,可通过环境变量切换:
```powershell
$env:E2E_PUSH_STRATEGY_TYPE="多工作室同时分发"
pytest -m e2e_push_003 --alluredir=reports/allure-results
```
可选策略类型:
```text
单平台指定
多工作室同时分发
多平台分发
```
## 说明
- 注册密码统一为 `test123456`,可在 `.env``E2E_REGISTER_PASSWORD` 调整。
- 手机号默认随机生成;如页面提示已注册,脚本会尝试退出并换号重新注册。
- 短信验证码支持 `manual``feishu``static` 三种模式;当前建议先用 `manual`,脚本会提示你在终端输入飞书测试通知群里的验证码。
- 页面选择器已按常见中文表单和按钮写成兜底形式;如果实际页面字段文案不同,只需要在 `e2e/pages/` 中补充定位器。
B<html xmlns:v="urn:schemas-microsoft-com:vml" B<html xmlns:v="urn:schemas-microsoft-com:vml"
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a:clrMap xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>
\ No newline at end of file
<xml xmlns:o="urn:schemas-microsoft-com:office:office">
<o:MainFile HRef="../SaaS平台关系模型.doc"/>
<o:File HRef="themedata.thmx"/>
<o:File HRef="colorschememapping.xml"/>
<o:File HRef="filelist.xml"/>
</xml>
\ No newline at end of file
# E2E Bug - E2E-MAIN-001
- 用例: `E2E-MAIN-001`
- 登录账号/手机号: `19988611833`
- 报错地点: `租户端商户认证-上传认证资质`
- 页面地址: `https://test-lianlianyun.lianlianwz.com/merchant/certification/apply`
- 截图: `BUG-E2E-MAIN-001-ffa51db250fa.png`
- 缺陷指纹: `ffa51db250fa`
- 首次/最近记录时间: `2026-06-16T11:30:05`
- 出现次数: `8`
## 前端页面错误
- 上传失败
## 后台接口错误
- 未检测到 4xx/5xx XHR/fetch 响应
\ No newline at end of file
# E2E Bug - E2E-ORDER-001
- 用例: `E2E-ORDER-001`
- 登录账号/手机号: `丸掌柜手工录单`
- 报错地点: `丸掌柜-商家管理-订单管理-手工录单-提交订单接口:E2EORDER0604180124/E2E手工录单0604180124`
- 页面地址: `https://test-merchant-lianlianyun.lianlianwz.com/productConsole?productCode=P20251204180120231330`
- 截图: `BUG-E2E-ORDER-001-70647a8630ad.png`
- 缺陷指纹: `70647a8630ad`
- 首次/最近记录时间: `2026-06-04T18:02:22`
- 出现次数: `4`
## 前端页面错误
- 未检测到明确前端错误文案
## 后台接口错误
- `400` `POST` `fetch` https://test-order-pc.lianlianwz.com/api/v1/order/create
- Request Body: `{"timestamp":1780567336743,"token":"eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE4MDY0ODcyOTYsIm1lcmNoYW50T3BlbklkIjoiN2ViZWU0NTQ1NjhkMDBkNjgyNjE0MTcxMTQ0NCIsIm1lcmNoYW50VXNlck9wZW5JZCI6ImQyMzI3ZDk4NzE2MGJmMWZjMGU4Mjk3MGZlNDciLCJuaWNrbmFtZSI6Ik1VMjAyNjA2MDQxNDI1NDI1MjAxNjAiLCJtZXJjaGFudE5vIjoiTTIwMjYwNjA0MTQyNTQyNTE4MTYwIn0.VNarLt_iCGO8bfSYVxA1Ume8Zr9is1FCwP6Pu_c3m7M","data":{"title":"E2E手工录单0604180124","amount":80,"orderHours":14,"gameRole":"E2E角色0124","gameAccount":"41275199","gamePassword":"test123456","contactInfo":"13800138000","crmUserQq":"41275199","crmUserWx":"17740687725","remark":"E2E手工录单 E2EORDER0604180124","gameBisServeType":1,"channelId":"manual_entry","channelName":"手工录单","gameId":18,"gameName":"王者","gameRegionId":1,"gameRegionName":"安卓QQ","gameServerId":3,"gameServerName":"国服","orderSource":"pdd","orderSourceName":"拼多多","orderSourceNumber":"E2EORDER0604180124","crmUserPhone":"13800138000","groupQrCode":""}}`
- Response Body: `{"success":false,"code":"0","message":"游戏代练项目不能为空","result":null}`
\ No newline at end of file
# E2E Bug - E2E-SAAS-001
- 用例: `E2E-SAAS-001`
- 登录账号/手机号: `19798577901`
- 报错地点: `租户端商户认证-上传认证资质`
- 页面地址: `https://test-lianlianyun.lianlianwz.com/merchant/certification/apply`
- 截图: `BUG-E2E-SAAS-001-329ced273cd4.png`
- 缺陷指纹: `329ced273cd4`
- 首次/最近记录时间: `2026-06-05T15:59:56`
- 出现次数: `11`
## 前端页面错误
- 上传失败
## 后台接口错误
- 未检测到 4xx/5xx XHR/fetch 响应
\ No newline at end of file
# E2E Bug - E2E-SAAS
- 用例: `E2E-SAAS`
- 登录账号/手机号: `丸掌柜撤销派单`
- 报错地点: `丸掌柜-商家管理-订单管理-撤销派单接口:E2EORDER0605115628/E2E手工录单0605115628`
- 页面地址: `https://test-merchant-lianlianyun.lianlianwz.com/productConsole?productCode=P20251204180120231330`
- 截图: `BUG-E2E-SAAS-908e6f985aad.png`
- 缺陷指纹: `908e6f985aad`
- 首次/最近记录时间: `2026-06-05T12:50:27`
- 出现次数: `2`
## 前端页面错误
- 未检测到明确前端错误文案
## 后台接口错误
- `404` `POST` `fetch` https://test-order-pc.lianlianwz.com/api/v1/order/cancelPushOrder
- Request Body: `{"timestamp":1780635024416,"token":"eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE4MDY1NTUwMTYsIm1lcmNoYW50T3BlbklkIjoiN2ViZWU0NTQ1NjhkMDBkNjgyNjE0MTcxMTQ0NCIsIm1lcmNoYW50VXNlck9wZW5JZCI6ImQyMzI3ZDk4NzE2MGJmMWZjMGU4Mjk3MGZlNDciLCJuaWNrbmFtZSI6Ik1VMjAyNjA2MDQxNDI1NDI1MjAxNjAiLCJtZXJjaGFudE5vIjoiTTIwMjYwNjA0MTQyNTQyNTE4MTYwIn0.4arfbSHiO11Ytb6cxJaZSLwvmP4AT6YibbWT9IQy-5k","data":{"orderNo":"BO20260605115711310150"}}`
- Response Body: `{"timestamp":1780635024087,"status":404,"error":"Not Found","path":"/v1/order/cancelPushOrder"}`
\ No newline at end of file
# E2E Bug - E2E-TASK-001
- 用例: `E2E-TASK-001`
- 登录账号/手机号: `19667441530`
- 报错地点: `丸友集-打手应用-我的订单菜单`
- 页面地址: `https://test-merchant-lianlianyun.lianlianwz.com/productConsole?productCode=P20250814095939369041`
- 关联订单: `E2EORDER0609161801 / E2E手工录单0609161801`
- 缺陷指纹: `af73fc00c95a`
- 首次/最近记录时间: `2026-06-10T11:45:00`
- 出现次数: `1`
## 前端页面错误
- 已复核:该问题不是产品缺陷。
- 人工操作观察记录显示,打手账号页面可展示 `打手应用-我的订单` 菜单,并且 `我的订单` 列表中可看到关联订单。
- 订单验证信息:
- `渠道单号:E2EORDER0609161801`
- `打手单号:WY20260609164952941052`
- 状态:`代练中`
- 订单来源:`外部应用`
- 原失败原因是自动化定位未覆盖实际页面形态:`我的订单` 业务 iframe 为 `https://test-booster.lianlianwz.com/orderManagement/order`,列表字段为 `渠道单号/打手单号`
## 后台接口错误
- 未检测到明确 4xx/5xx XHR/fetch 响应。
- 非后台接口问题;自动化已改为识别 `渠道单号/打手单号/代练中`,并增加直达 `orderManagement/order` 的兜底验证。
# E2E Bug - E2E-TASK-001
- 用例: `E2E-TASK-001`
- 登录账号/手机号: `19667441530`
- 报错地点: `丸友集-打手应用-抢单大厅-立即抢单`
- 页面地址: `https://test-merchant-lianlianyun.lianlianwz.com/productConsole?productCode=P20250814095939369041`
- 关联订单: `E2EORDER0609161801 / E2E手工录单0609161801`
- 缺陷指纹: `d15502a47e8c`
- 首次/最近记录时间: `2026-06-10T11:30:00`
- 出现次数: `1`
## 前端页面错误
- 打手点击【立即抢单】并完成验证码确认后,抢单大厅页面未成功自动刷新。
- 页面仍可能停留在抢单大厅旧列表状态,继续展示当前订单或【立即抢单】入口,导致自动化和人工观察都容易误判为抢单未成功。
- 由于刷新前旧列表仍显示【立即抢单】,自动化如果继续依据抢单大厅列表判断,会重复打开验证码并重复抢同一单。
- 当前自动化已调整为:验证码确认成功后,不再依赖抢单大厅列表刷新;直接进入 `打手应用-我的订单` 验证订单是否已流转。
## 后台接口错误
- 未检测到明确 4xx/5xx XHR/fetch 响应。
- 该问题表现为前端状态未及时刷新,需前端在抢单成功后刷新列表或移除已抢订单。
# Skill + MCP + Agent Automation Architecture
This repository now has a lightweight automation layer around the existing pytest + Playwright + Allure suite.
## Layers
| Layer | Path | Purpose |
| --- | --- | --- |
| Skill | `.codex/skills/saas-e2e-automation/` | Gives Codex repository-specific E2E workflow knowledge. |
| MCP | `scripts/mcp_saas_e2e.py` and `.codex/mcp/saas-e2e.json` | Exposes test planning, account inspection, bug lookup, report summary, and pytest runs as tools. |
| Agent entry | `scripts/agent_e2e.py` | Human-friendly CLI for the same core operations when MCP is not attached. |
| Core library | `scripts/e2e_automation_core.py` | Shared implementation used by both MCP and CLI. |
| Agent profile | `.codex/agents/e2e-runner.md` | Operating loop for future test-running/debugging agents. |
## Daily Commands
```powershell
.\.venv\Scripts\python.exe .\scripts\agent_e2e.py plan standalone
.\.venv\Scripts\python.exe .\scripts\agent_e2e.py accounts
.\.venv\Scripts\python.exe .\scripts\agent_e2e.py run main
.\.venv\Scripts\python.exe .\scripts\agent_e2e.py summary
.\.venv\Scripts\python.exe .\scripts\agent_e2e.py bugs
```
## MCP Tools
The `saas-e2e` MCP server supports:
- `list_tests`
- `inspect_accounts`
- `latest_bugs`
- `allure_summary`
- `build_plan`
- `run_pytest`
Point a Codex/MCP client at `.codex/mcp/saas-e2e.json`, or copy that server entry into the client-level MCP config.
from __future__ import annotations
import os
from dataclasses import dataclass
import pytest
from dotenv import load_dotenv
from playwright.sync_api import Page
from e2e.utils.bug_reporter import BugReporter
load_dotenv()
@dataclass(frozen=True)
class E2ESettings:
tenant_base_url: str
tenant_home_url: str
admin_url: str
admin_username: str
admin_password: str
register_password: str
phone_prefix: str
max_register_attempts: int
enterprise_name_prefix: str
verification_code_mode: str
verification_code_static: str
manual_code_file: str
manual_code_timeout_seconds: int
manual_code_poll_interval_seconds: int
image_captcha_mode: str
feishu_app_id: str
feishu_app_secret: str
feishu_chat_id: str
feishu_timeout_seconds: int
feishu_poll_interval_seconds: int
certified_tenant_phone: str
certified_tenant_password: str
certified_tenant_enterprise_name: str
certified_tenant_merchant_id: str
wanyouji_product_id: str
wanzhanggui_product_id: str
wanzhanggui_tenant_phone: str
wanzhanggui_tenant_password: str
viewport_width: int
viewport_height: int
@pytest.fixture(scope="session")
def settings() -> E2ESettings:
return E2ESettings(
tenant_base_url=os.getenv("TENANT_BASE_URL", "https://test-lianlianyun.lianlianwz.com"),
tenant_home_url=os.getenv("TENANT_HOME_URL", "https://test-lianlianyun.lianlianwz.com/home"),
admin_url=os.getenv("ADMIN_URL", "https://test-saas-adminui.lianlianwz.com/pages/home/index"),
admin_username=os.getenv("ADMIN_USERNAME", "wangss"),
admin_password=os.getenv("ADMIN_PASSWORD", "123456"),
register_password=os.getenv("E2E_REGISTER_PASSWORD", "test123456"),
phone_prefix=os.getenv("E2E_PHONE_PREFIX", "19"),
max_register_attempts=int(os.getenv("E2E_MAX_REGISTER_ATTEMPTS", "5")),
enterprise_name_prefix=os.getenv("E2E_ENTERPRISE_NAME_PREFIX", "E2E测试企业"),
verification_code_mode=os.getenv("E2E_VERIFICATION_CODE_MODE", "feishu"),
verification_code_static=os.getenv("E2E_VERIFICATION_CODE_STATIC", ""),
manual_code_file=os.getenv("E2E_MANUAL_CODE_FILE", "runtime/sms_code.txt"),
manual_code_timeout_seconds=int(os.getenv("E2E_MANUAL_CODE_TIMEOUT_SECONDS", "180")),
manual_code_poll_interval_seconds=int(os.getenv("E2E_MANUAL_CODE_POLL_INTERVAL_SECONDS", "2")),
image_captcha_mode=os.getenv("E2E_IMAGE_CAPTCHA_MODE", "ddddocr"),
feishu_app_id=os.getenv("FEISHU_APP_ID", ""),
feishu_app_secret=os.getenv("FEISHU_APP_SECRET", ""),
feishu_chat_id=os.getenv("FEISHU_CHAT_ID", ""),
feishu_timeout_seconds=int(os.getenv("FEISHU_TIMEOUT_SECONDS", "60")),
feishu_poll_interval_seconds=int(os.getenv("FEISHU_POLL_INTERVAL_SECONDS", "3")),
certified_tenant_phone=os.getenv("E2E_CERTIFIED_TENANT_PHONE", "19361091624"),
certified_tenant_password=os.getenv("E2E_CERTIFIED_TENANT_PASSWORD", "test123456"),
certified_tenant_enterprise_name=os.getenv("E2E_CERTIFIED_TENANT_ENTERPRISE_NAME", "E2E测试企业0603174618802"),
certified_tenant_merchant_id=os.getenv("E2E_CERTIFIED_TENANT_MERCHANT_ID", "99"),
wanyouji_product_id=os.getenv("E2E_WANYOUJI_PRODUCT_ID", "12"),
wanzhanggui_product_id=os.getenv("E2E_WANZHANGGUI_PRODUCT_ID", "15"),
wanzhanggui_tenant_phone=os.getenv("E2E_WANZHANGGUI_TENANT_PHONE", "19147509308"),
wanzhanggui_tenant_password=os.getenv("E2E_WANZHANGGUI_TENANT_PASSWORD", "test123456"),
viewport_width=int(os.getenv("E2E_VIEWPORT_WIDTH", "1280")),
viewport_height=int(os.getenv("E2E_VIEWPORT_HEIGHT", "900")),
)
@pytest.fixture(scope="session")
def browser_type_launch_args(browser_type_launch_args, settings: E2ESettings):
args = list(browser_type_launch_args.get("args", []))
window_size_arg = f"--window-size={settings.viewport_width},{settings.viewport_height}"
args = [arg for arg in args if not str(arg).startswith("--window-size=")]
args.append(window_size_arg)
return {
**browser_type_launch_args,
"args": args,
}
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args, settings: E2ESettings):
return {
**browser_context_args,
"viewport": {
"width": settings.viewport_width,
"height": settings.viewport_height,
},
"screen": {
"width": settings.viewport_width,
"height": settings.viewport_height,
},
}
@pytest.fixture
def bug_reporter(page: Page, request) -> BugReporter:
marker_to_case = {
"e2e_saas_001": "E2E-SAAS-001",
"e2e_saas_003": "E2E-SAAS-003",
"e2e_saas_004": "E2E-SAAS-004",
"e2e_auth_closed_loop": "E2E-AUTH-001",
"e2e_auth_new_enterprise": "E2E-AUTH-NEW-ENTERPRISE",
"e2e_push_001": "E2E-PUSH-001",
"e2e_push_003": "E2E-PUSH-003",
"e2e_order_001": "E2E-ORDER-001",
"e2e_task_001": "E2E-TASK-001",
"e2e_member_001": "E2E-MEMBER-001",
"e2e_member_002": "E2E-MEMBER-002",
"e2e_main_001": "E2E-MAIN-001",
"e2e_main_002": "E2E-MAIN-002",
}
case_id = "E2E-SAAS"
for marker_name, marker_case_id in marker_to_case.items():
if request.node.get_closest_marker(marker_name):
case_id = marker_case_id
break
return BugReporter(page, case_id=case_id)
from __future__ import annotations
from playwright.sync_api import Page
from e2e.pages.admin_page import AdminPage
class AdminApplicationPage(AdminPage):
def open_application_request_list(self) -> None:
self.maybe_click_text(["产品管理"], timeout=3000)
self.click_exact_text("产品申请列表", timeout=5000)
self.page.wait_for_load_state("domcontentloaded")
self.page.wait_for_timeout(2000)
def approve_product_application(
self,
enterprise_name: str,
product_name: str,
merchant_id: str = "",
product_id: str = "",
) -> None:
self.open_application_request_list()
if merchant_id or product_id:
self.search_product_application(merchant_id=merchant_id, product_id=product_id)
elif enterprise_name:
self.search_product_application(keyword=enterprise_name)
row = self.find_application_row(enterprise_name, product_name, merchant_id=merchant_id, product_id=product_id)
if row is None:
row = self.find_application_row("", product_name, merchant_id=merchant_id, product_id=product_id)
if row is None and product_id:
row = self.find_application_row("", "", merchant_id=merchant_id, product_id=product_id)
if row is None:
approved_row = self.find_application_row(
enterprise_name,
product_name,
merchant_id=merchant_id,
product_id=product_id,
accepted_statuses=["审核通过", "已通过", "已开通"],
)
if approved_row is not None:
return
raise AssertionError(f"主站产品申请列表未找到 {enterprise_name} 的 {product_name} 待审核申请")
for action in ("审核", "同意", "通过", "查看"):
action_button = row.get_by_text(action, exact=True).first
if action_button.count():
action_button.click()
break
else:
raise AssertionError(f"主站产品申请列表中 {product_name} 申请行未找到审批入口")
self.page.wait_for_timeout(1000)
self.submit_product_application_audit()
self.page.wait_for_timeout(2000)
def submit_product_application_audit(self) -> None:
remark = self.page.locator(".ant-modal textarea[placeholder='请输入审核备注']").first
if remark.count():
remark.fill("E2E自动化审核通过")
self.click_first_visible(
[
".ant-modal button:has-text('提 交')",
".ant-modal button:has-text('提交')",
"button:has-text('提 交')",
"button:has-text('提交')",
],
timeout=5000,
)
self.maybe_click_text(["确定", "确认"], timeout=3000)
def search_product_application(self, merchant_id: str = "", product_id: str = "", keyword: str = "") -> None:
if merchant_id:
self.fill_first_visible(["input[placeholder='请输入商户ID']"], merchant_id, timeout=3000)
if product_id:
self.fill_first_visible(["input[placeholder='请输入产品ID']"], product_id, timeout=3000)
if keyword:
try:
self.fill_first_visible(
[
"input[placeholder*='商户名称']",
"input[placeholder*='企业名称']",
"input[placeholder*='用户名称']",
"input[placeholder*='名称']",
],
keyword,
timeout=2000,
)
except AssertionError:
pass
self.click_exact_text("查询", timeout=3000)
self.page.wait_for_timeout(2000)
def filter_application_status(self, statuses: list[str]) -> None:
selector = self.page.locator(".ant-select-selector").filter(has_text="申请状态").first
if not selector.count():
selector = self.page.locator(".ant-select-selector").first
if not selector.count():
return
for status in statuses:
try:
selector.click(timeout=1000)
self.page.get_by_text(status, exact=True).first.click(timeout=1000)
self.page.wait_for_timeout(1000)
return
except Exception:
continue
def find_application_row(
self,
enterprise_name: str,
product_name: str,
merchant_id: str = "",
product_id: str = "",
accepted_statuses: list[str] | None = None,
):
accepted_statuses = accepted_statuses or ["待审核", "审核中", "申请中", "待通过", "待审批", "审核中"]
rows = self.page.locator(".ant-table-row")
for index in range(rows.count()):
row = rows.nth(index)
text = row.inner_text(timeout=1000)
columns = [item.strip() for item in text.split("\t")]
if merchant_id and merchant_id not in columns:
continue
if product_id and product_id not in columns:
continue
if product_name and not product_id and product_name not in text:
continue
if enterprise_name and enterprise_name not in text:
continue
if not any(status in text for status in accepted_statuses):
continue
return row
return None
from __future__ import annotations
from playwright.sync_api import Page
from e2e.pages.base_page import BasePage
class AdminPage(BasePage):
def __init__(self, page: Page, admin_url: str):
super().__init__(page)
self.admin_url = admin_url
def login(self, username: str, password: str) -> None:
login_url = self.admin_url.replace("/pages/home/index", "/pages/login/index")
self.goto(login_url)
self.page.wait_for_load_state("domcontentloaded")
self.page.wait_for_timeout(1500)
self.fill_first_visible(
[
"input[placeholder*='请输入账号']",
"input[placeholder*='账号']",
"input[placeholder*='用户名']",
"input[name='username']",
"input[type='text']",
],
username,
timeout=10000,
)
self.fill_first_visible(
[
"input[placeholder*='请输入密码']",
"input[placeholder*='密码']",
"input[name='password']",
"input[type='password']",
],
password,
timeout=10000,
)
self.click_first_visible(
[
"taro-button-core:has-text('登录')",
"button:has-text('登录')",
"button:has-text('登 录')",
"button[type='submit']",
]
)
def approve_enterprise_certification(self, enterprise_name: str) -> None:
self.maybe_click_text(["商户管理", "企业管理", "租户管理", "认证管理"], timeout=3000)
self.maybe_click_text(["商户认证", "企业认证", "认证审核", "待审核"], timeout=3000)
search_input = self.page.locator(
"input[placeholder*='用户名称'], input[placeholder*='用户'], input[placeholder*='企业'], input[placeholder*='商户'], input[placeholder*='名称'], input[placeholder*='关键字']"
).first
if search_input.count():
search_input.fill(enterprise_name)
self.maybe_click_text(["查询", "搜索"], timeout=1000)
row = self.page.locator(".ant-table-row", has_text=enterprise_name).first
row.wait_for(state="visible", timeout=10000)
audit_button = row.get_by_text("审核", exact=True).first
if audit_button.count():
audit_button.click()
else:
row.get_by_text("查看", exact=True).first.click()
self.maybe_click_text(["审核"], timeout=1500)
self.page.locator(".ant-modal .ant-select-selector").click()
self.page.get_by_text("通过", exact=True).click()
self.click_first_visible([".ant-modal button:has-text('提 交')", ".ant-modal button:has-text('提交')"], timeout=5000)
self.maybe_click_text(["确定", "确认"], timeout=3000)
from __future__ import annotations
from collections.abc import Iterable
from playwright.sync_api import Locator, Page, expect
class BasePage:
def __init__(self, page: Page):
self.page = page
def goto(self, url: str) -> None:
self.page.goto(url, wait_until="domcontentloaded")
def click_first_visible(self, selectors: Iterable[str], timeout: int = 3000) -> Locator:
locator = self.find_first_visible(selectors, timeout=timeout)
locator.click()
return locator
def fill_first_visible(self, selectors: Iterable[str], value: str, timeout: int = 3000) -> Locator:
locator = self.find_first_visible(selectors, timeout=timeout)
locator.fill(value)
return locator
def fill_and_verify(self, locator: Locator, value: str, timeout: int = 5000) -> None:
expect(locator).to_be_visible(timeout=timeout)
last_value = ""
for _ in range(4):
locator.click(force=True)
locator.press("Control+A")
locator.fill(value)
try:
expect(locator).to_have_value(value, timeout=1200)
return
except Exception:
try:
locator.click(force=True)
locator.press("Control+A")
locator.type(value, delay=30)
expect(locator).to_have_value(value, timeout=1200)
return
except Exception:
try:
last_value = locator.input_value(timeout=1000)
except Exception:
last_value = "<unreadable>"
self.page.wait_for_timeout(500)
raise AssertionError(f"输入框未成功写入期望值 {value},最后读取值:{last_value}")
def find_first_visible(self, selectors: Iterable[str], timeout: int = 3000) -> Locator:
last_error: Exception | None = None
for selector in selectors:
locators = self.page.locator(selector)
try:
count = locators.count()
except Exception as exc:
last_error = exc
continue
for index in range(count):
locator = locators.nth(index)
try:
expect(locator).to_be_visible(timeout=timeout)
return locator
except Exception as exc: # Playwright 不同版本会抛出不同的断言异常。
last_error = exc
raise AssertionError(f"未找到可见选择器,已尝试:{list(selectors)}") from last_error
def text_visible(self, text: str, timeout: int = 3000) -> bool:
try:
expect(self.page.get_by_text(text).first).to_be_visible(timeout=timeout)
return True
except Exception:
return False
def maybe_click_text(self, texts: Iterable[str], timeout: int = 1500) -> bool:
for text in texts:
locator = self.page.get_by_text(text, exact=False).first
try:
expect(locator).to_be_visible(timeout=timeout)
locator.click()
return True
except Exception:
continue
return False
def click_exact_text(self, text: str, timeout: int = 3000) -> Locator:
locators = self.page.get_by_text(text, exact=True)
last_error: Exception | None = None
for index in range(locators.count()):
locator = locators.nth(index)
try:
expect(locator).to_be_visible(timeout=timeout)
locator.click()
return locator
except Exception as exc:
last_error = exc
raise AssertionError(f"未找到可见的精确文本:{text}") from last_error
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
from __future__ import annotations
import os
from typing import Any
import pytest
from playwright.sync_api import Page
from e2e.pages.wanzhanggui_order_page import WanyoujiFulfillmentPage
from e2e.tests.订单流转.test_e2e_task_001_external_order_dispatch_to_player import (
login_player_sub_account_by_sms,
resolve_task_order_and_accounts,
)
from e2e.utils.account_store import E2EAccount, account_store
@pytest.mark.e2e_delivery_001
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_打手确认上号上传首图并提交验收后主账号查看(page: Page, settings, bug_reporter) -> None:
store = account_store()
order, wanyouji_account, player_account = resolve_delivery_order_and_accounts()
source_order_no = order["source_order_no"]
order_title = order["order_title"]
login_player_sub_account_by_sms(page, settings, bug_reporter, player_account)
player_page = WanyoujiFulfillmentPage(page, settings.tenant_home_url, bug_reporter)
player_page.submit_player_delivery_for_acceptance(source_order_no, order_title)
store.mark_task_order_status(
source_order_no=source_order_no,
status="delivery_submitted",
task_player_phone=player_account.phone,
)
bug_reporter.record_if_errors(
account=player_account.phone,
location=f"丸友集-打手应用-我的订单-确认上号-上传首图-提交验收:{source_order_no}/{order_title}",
)
owner_page = WanyoujiFulfillmentPage(page, settings.tenant_home_url, bug_reporter)
owner_page.login_main_account(wanyouji_account.phone, wanyouji_account.password)
frame = owner_page.open_workshop_order_list()
text = owner_page.search_workshop_order_list(frame, source_order_no, order_title)
assert source_order_no in text or order_title in text, (
f"丸友集主账号订单列表未找到打手提交验收后的订单;source_order_no={source_order_no}; "
f"order_title={order_title}; text={text[:1500]}"
)
assert "待验收" in text or "申请上游验收" in text, (
f"丸友集主账号订单列表未看到待验收/申请上游验收状态;source_order_no={source_order_no}; "
f"text={text[:1500]}"
)
store.mark_task_order_status(
source_order_no=source_order_no,
status="delivery_submitted",
delivery_checked_by_wanyouji_owner=wanyouji_account.phone,
)
bug_reporter.record_if_errors(
account=wanyouji_account.phone,
location=f"丸友集主账号-工作室履约应用-订单列表-查看待验收订单:{source_order_no}/{order_title}",
)
def resolve_delivery_order_and_accounts() -> tuple[dict[str, Any], E2EAccount, E2EAccount]:
explicit_order_no = os.getenv("E2E_DELIVERY_SOURCE_ORDER_NO", "").strip()
if explicit_order_no:
os.environ["E2E_TASK_SOURCE_ORDER_NO"] = explicit_order_no
return resolve_task_order_and_accounts(os.getenv("E2E_TASK_WANZHANGGUI_PHONE", "19147509308"))
store = account_store()
payload = store.load_payload()
candidates = [
item
for item in payload.get("dispatched_orders", [])
if item.get("status") == "task_player_accepted"
and item.get("task_player_phone")
and (item.get("task_wanyouji_phone") or item.get("wanyouji_phones"))
]
candidates = sorted(candidates, key=lambda item: item.get("updated_at", item.get("created_at", "")), reverse=True)
assert candidates, "台账中没有可用于 E2E-DELIVERY-001 的已接单订单,请先执行 E2E-TASK-001。"
order = candidates[0]
os.environ["E2E_TASK_SOURCE_ORDER_NO"] = order["source_order_no"]
return resolve_task_order_and_accounts(os.getenv("E2E_TASK_WANZHANGGUI_PHONE", "19147509308"))
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
$ErrorActionPreference = 'SilentlyContinue'
$items = Get-Process EXCEL -ErrorAction SilentlyContinue | Where-Object { -not $_.MainWindowTitle }
foreach ($item in $items) {
Write-Output ('stopping_hidden_excel=' + $item.Id)
Stop-Process -Id $item.Id -Force
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment