feat: 完成Chrome启动器
- 新增 app/chrome.py 拼装远程调试启动参数并启动 Chrome - 实现 /json/version 端口探测并禁用环境代理 - 新增 chrome 单元测试覆盖参数、Popen 和端口就绪 - 更新任务看板、API 合约、当前状态和进度记录
This commit is contained in:
+115
@@ -0,0 +1,115 @@
|
|||||||
|
"""Chrome launcher helpers for per-account CDP sessions."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
from . import appconfig
|
||||||
|
from . import config as account_config
|
||||||
|
|
||||||
|
|
||||||
|
CDP_HOST = "127.0.0.1"
|
||||||
|
|
||||||
|
|
||||||
|
class ChromeLaunchError(RuntimeError):
|
||||||
|
"""Raised when Chrome launch configuration is invalid."""
|
||||||
|
|
||||||
|
|
||||||
|
def _account_value(account, field, default=None):
|
||||||
|
if isinstance(account, dict):
|
||||||
|
return account.get(field, default)
|
||||||
|
return getattr(account, field, default)
|
||||||
|
|
||||||
|
|
||||||
|
def _required_account_value(account, field):
|
||||||
|
value = _account_value(account, field)
|
||||||
|
if value is None or str(value).strip() == "":
|
||||||
|
raise ChromeLaunchError(f"账号缺少字段: {field}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _debug_port(account) -> int:
|
||||||
|
value = int(_required_account_value(account, "debug_port"))
|
||||||
|
if value <= 0 or value > 65535:
|
||||||
|
raise ChromeLaunchError("debug_port 必须在 1-65535 范围内")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _user_data_dir(account, config=None) -> str:
|
||||||
|
existing = _account_value(account, "user_data_dir")
|
||||||
|
if existing:
|
||||||
|
path = os.path.abspath(str(existing))
|
||||||
|
os.makedirs(path, exist_ok=True)
|
||||||
|
return path
|
||||||
|
|
||||||
|
slug = _account_value(account, "slug")
|
||||||
|
if not slug:
|
||||||
|
slug = account_config.make_slug(_required_account_value(account, "alias"))
|
||||||
|
return account_config.ensure_user_data_dir(slug, config=config)
|
||||||
|
|
||||||
|
|
||||||
|
def build_launch_args(account, config=None) -> list:
|
||||||
|
"""Build Chrome command arguments for one account."""
|
||||||
|
|
||||||
|
chrome_path = appconfig.chrome_path(config)
|
||||||
|
if not chrome_path:
|
||||||
|
raise ChromeLaunchError("chrome_path 不能为空")
|
||||||
|
port = _debug_port(account)
|
||||||
|
user_data_dir = _user_data_dir(account, config=config)
|
||||||
|
return [
|
||||||
|
chrome_path,
|
||||||
|
f"--remote-debugging-port={port}",
|
||||||
|
"--remote-allow-origins=*",
|
||||||
|
f"--user-data-dir={user_data_dir}",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def launch_chrome(account, config=None) -> subprocess.Popen:
|
||||||
|
"""Launch Chrome for one account and return the process handle."""
|
||||||
|
|
||||||
|
args = build_launch_args(account, config=config)
|
||||||
|
try:
|
||||||
|
return subprocess.Popen(args)
|
||||||
|
except OSError as exc:
|
||||||
|
raise ChromeLaunchError(f"启动 Chrome 失败: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _json_version_url(port, host=CDP_HOST):
|
||||||
|
return f"http://{host}:{int(port)}/json/version"
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_json_version(port, host=CDP_HOST, timeout=1.0):
|
||||||
|
request = urllib.request.Request(_json_version_url(port, host=host), method="GET")
|
||||||
|
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||||
|
with opener.open(request, timeout=timeout) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
raise ChromeLaunchError(f"CDP 返回状态码: {response.status}")
|
||||||
|
body = response.read()
|
||||||
|
return json.loads(body.decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def is_running(port, host=CDP_HOST) -> bool:
|
||||||
|
"""Return whether a Chrome CDP endpoint responds on /json/version."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
_fetch_json_version(port, host=host, timeout=1.0)
|
||||||
|
return True
|
||||||
|
except (OSError, urllib.error.URLError, json.JSONDecodeError, ChromeLaunchError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def wait_debug_ready(port, timeout=60, host=CDP_HOST) -> bool:
|
||||||
|
"""Poll until the Chrome CDP endpoint is ready or timeout expires."""
|
||||||
|
|
||||||
|
deadline = time.monotonic() + float(timeout)
|
||||||
|
while True:
|
||||||
|
if is_running(port, host=host):
|
||||||
|
return True
|
||||||
|
if time.monotonic() >= deadline:
|
||||||
|
return False
|
||||||
|
time.sleep(0.25)
|
||||||
+1
-1
@@ -36,7 +36,7 @@
|
|||||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| T-101 | `config` 生成 slug + 创建 `chrome_user_data_dir/<slug>` | T-003 | 别名→唯一 slug;目录按需建;路径绝对化 | DONE |
|
| T-101 | `config` 生成 slug + 创建 `chrome_user_data_dir/<slug>` | T-003 | 别名→唯一 slug;目录按需建;路径绝对化 | DONE |
|
||||||
| T-102 | `app/chrome.py` 启动器:拼参数并启动、探测端口 | T-101, T-002 | 含三参数;端口就绪可探测 | TODO |
|
| T-102 | `app/chrome.py` 启动器:拼参数并启动、探测端口 | T-101, T-002 | 含三参数;端口就绪可探测 | DONE |
|
||||||
| T-103 | 首次登录保活 + 登录检测 `is_logged_in` | T-102, T-001 | 关闭再启动免重登;登录/未登录判断准确 | TODO |
|
| T-103 | 首次登录保活 + 登录检测 `is_logged_in` | T-102, T-001 | 关闭再启动免重登;登录/未登录判断准确 | TODO |
|
||||||
| T-104 | PySide6 五 Tab 主窗口骨架(`QMainWindow` + `QTabWidget`,5 Tab 空壳) | T-002 | 五个 Tab 按顺序可切换;启动不阻塞;基础状态栏可用 | TODO |
|
| T-104 | PySide6 五 Tab 主窗口骨架(`QMainWindow` + `QTabWidget`,5 Tab 空壳) | T-002 | 五个 Tab 按顺序可切换;启动不阻塞;基础状态栏可用 | TODO |
|
||||||
| T-104b | PySide6 worker 基类与线程启动工具(`BaseWorker` + `QThread` 包装) | T-104 | signals: progress/log/row_updated/failed/finished/cancelled;取消标记可用;worker 不直接操作 QWidget | TODO |
|
| T-104b | PySide6 worker 基类与线程启动工具(`BaseWorker` + `QThread` 包装) | T-104 | signals: progress/log/row_updated/failed/finished/cancelled;取消标记可用;worker 不直接操作 QWidget | TODO |
|
||||||
|
|||||||
+9
-6
@@ -121,16 +121,19 @@ ensure_user_data_dir(slug, root=None, config=None) -> str
|
|||||||
|
|
||||||
`make_slug()` 使用可读 ASCII 前缀 + 8 位 SHA1 后缀,保证稳定且降低别名冲突;非 ASCII 别名使用 `account_<hash>`。`ensure_user_data_dir()` 拒绝非 `[a-z0-9_]` slug,防止路径穿越。
|
`make_slug()` 使用可读 ASCII 前缀 + 8 位 SHA1 后缀,保证稳定且降低别名冲突;非 ASCII 别名使用 `account_<hash>`。`ensure_user_data_dir()` 拒绝非 `[a-z0-9_]` slug,防止路径穿越。
|
||||||
|
|
||||||
## chrome 模块(`app/chrome.py`,待建)
|
## chrome 模块(`app/chrome.py`,已建)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
build_launch_args(account) -> list[str] # chrome + --remote-debugging-port + --remote-allow-origins=* + --user-data-dir
|
class ChromeLaunchError(RuntimeError): ...
|
||||||
launch_chrome(account) -> subprocess.Popen
|
build_launch_args(account, config=None) -> list[str]
|
||||||
wait_debug_ready(port, timeout=60) -> bool
|
# chrome + --remote-debugging-port + --remote-allow-origins=* + --user-data-dir
|
||||||
is_running(port) -> bool
|
launch_chrome(account, config=None) -> subprocess.Popen
|
||||||
create_shortcut(account, dest_dir=None) -> str # 可选 .lnk,PowerShell WScript.Shell
|
wait_debug_ready(port, timeout=60, host="127.0.0.1") -> bool
|
||||||
|
is_running(port, host="127.0.0.1") -> bool
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`build_launch_args()` 接受 dict 或对象形式账号;账号需有 `debug_port`,并有 `user_data_dir` 或 `slug/alias`。端口探测访问 `/json/version`,显式禁用环境代理。`.lnk` 快捷方式留给 T-106。
|
||||||
|
|
||||||
## cdp 模块(`app/cdp.py`,T-000 由根目录 `cdp.py` 迁入)
|
## cdp 模块(`app/cdp.py`,T-000 由根目录 `cdp.py` 迁入)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|||||||
+10
-6
@@ -6,10 +6,10 @@
|
|||||||
## 当前快照
|
## 当前快照
|
||||||
|
|
||||||
- 日期:2026-06-27
|
- 日期:2026-06-27
|
||||||
- 阶段:V0 单账号 CDP 流程已验证;V1 已完成 T-000 正式代码包结构、T-001 `app/editor.py` 模块化、T-002 `app/appconfig.py` 应用配置、T-003 SQLite 持久化地基、T-004 本地数据忽略规则、T-005 AI 模型清单后端、T-006 单元测试基座、T-101 账号 user-data-dir 工具。
|
- 阶段:V0 单账号 CDP 流程已验证;V1 已完成 T-000 正式代码包结构、T-001 `app/editor.py` 模块化、T-002 `app/appconfig.py` 应用配置、T-003 SQLite 持久化地基、T-004 本地数据忽略规则、T-005 AI 模型清单后端、T-006 单元测试基座、T-101 账号 user-data-dir 工具、T-102 Chrome 启动器。
|
||||||
- 技术栈:Python 3.10+,自研 CDP(websocket-client + requests),SQLite(sqlite3)+ `config.json` + openpyxl + AI(服务商待定),GUI PySide6 5 Tab(已定)。
|
- 技术栈:Python 3.10+,自研 CDP(websocket-client + requests),SQLite(sqlite3)+ `config.json` + openpyxl + AI(服务商待定),GUI PySide6 5 Tab(已定)。
|
||||||
- 生产代码:已建立 `app/` 包 + 根入口 `main.py`;`app/cdp.py` 为已验证 CDP 底座;`app/editor.py` 已封装标题/封面/采集/更新按钮能力;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数与端口读取,以及 `config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接;`app/db.py` 已实现 SQLite schema、连接 PRAGMA、批次/账号/任务与阶段写库函数;`app/config.py` 已实现账号 slug 与 user-data-dir 创建;`app/gui.py` 目前是入口占位,完整 PySide6 主窗口待 T-104。
|
- 生产代码:已建立 `app/` 包 + 根入口 `main.py`;`app/cdp.py` 为已验证 CDP 底座;`app/editor.py` 已封装标题/封面/采集/更新按钮能力;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数与端口读取,以及 `config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接;`app/db.py` 已实现 SQLite schema、连接 PRAGMA、批次/账号/任务与阶段写库函数;`app/config.py` 已实现账号 slug 与 user-data-dir 创建;`app/chrome.py` 已实现 Chrome 参数拼装、启动与 CDP 端口探测;`app/gui.py` 目前是入口占位,完整 PySide6 主窗口待 T-104。
|
||||||
- 测试:`tests/` 已建立;T-006 后纯逻辑改动必须运行 `python -m unittest discover -s tests`,当前覆盖 appconfig/db/config,并对尚未实现的 app.excel/app.prompts 做契约占位 skip;CDP/Shopee 改动仍需测试商品手动验证。
|
- 测试:`tests/` 已建立;T-006 后纯逻辑改动必须运行 `python -m unittest discover -s tests`,当前覆盖 appconfig/db/config/chrome,并对尚未实现的 app.excel/app.prompts 做契约占位 skip;CDP/Shopee 改动仍需测试商品手动验证。
|
||||||
- 数据:`config.json`、`config/ai_models.json`、`cmshopee.db`、`chrome_user_data_dir/`、`images/` 已由 `.gitignore` 排除;`app/appconfig.py` 首次读取缺失的 `config.json` 时会在本地写默认配置,`app/db.py` 调用 `init_db()` 时会在本地创建 SQLite DB。
|
- 数据:`config.json`、`config/ai_models.json`、`cmshopee.db`、`chrome_user_data_dir/`、`images/` 已由 `.gitignore` 排除;`app/appconfig.py` 首次读取缺失的 `config.json` 时会在本地写默认配置,`app/db.py` 调用 `init_db()` 时会在本地创建 SQLite DB。
|
||||||
|
|
||||||
## 既定设计要点(文档已定)
|
## 既定设计要点(文档已定)
|
||||||
@@ -37,8 +37,9 @@
|
|||||||
| `app/appconfig.py` | 已有 | T-002 产出:`config.json` 默认值、读写、更新、路径/端口/AI 参数读取;拒绝敏感字段写入 |
|
| `app/appconfig.py` | 已有 | T-002 产出:`config.json` 默认值、读写、更新、路径/端口/AI 参数读取;拒绝敏感字段写入 |
|
||||||
| `app/db.py` | 已有 | T-003 产出:batches/accounts/tasks schema;WAL/busy_timeout/foreign_keys;账号/批次/任务与 set_* 阶段写库 |
|
| `app/db.py` | 已有 | T-003 产出:batches/accounts/tasks schema;WAL/busy_timeout/foreign_keys;账号/批次/任务与 set_* 阶段写库 |
|
||||||
| `app/config.py` | 已有 | T-101 产出:别名→稳定 slug;创建并返回绝对 user-data-dir |
|
| `app/config.py` | 已有 | T-101 产出:别名→稳定 slug;创建并返回绝对 user-data-dir |
|
||||||
|
| `app/chrome.py` | 已有 | T-102 产出:Chrome 启动参数、`subprocess.Popen` 启动、`/json/version` 端口探测;快捷方式待 T-106 |
|
||||||
| `tests/` | 已有 | T-006 产出:stdlib unittest 基座;appconfig/db 单元测试;excel/prompts 模块契约占位测试 |
|
| `tests/` | 已有 | T-006 产出:stdlib unittest 基座;appconfig/db 单元测试;excel/prompts 模块契约占位测试 |
|
||||||
| `app/excel.py` / `app/chrome.py` / `app/workers.py` | 待建 | Phase 1-3 产出 |
|
| `app/excel.py` / `app/workers.py` | 待建 | Phase 1-3 产出 |
|
||||||
| `config.json` / `config/ai_models.json` / `cmshopee.db` / `chrome_user_data_dir/` / `images/` | 本地待建,已忽略 | 含配置、密钥、业务、登录态、图片,不提交版本库 |
|
| `config.json` / `config/ai_models.json` / `cmshopee.db` / `chrome_user_data_dir/` / `images/` | 本地待建,已忽略 | 含配置、密钥、业务、登录态、图片,不提交版本库 |
|
||||||
|
|
||||||
## 已验证能力(单账号)
|
## 已验证能力(单账号)
|
||||||
@@ -53,9 +54,9 @@
|
|||||||
|
|
||||||
任务状态以 [`06-tasks.md`](06-tasks.md) 为准,历史记录见 [`../progress.md`](../progress.md)。
|
任务状态以 [`06-tasks.md`](06-tasks.md) 为准,历史记录见 [`../progress.md`](../progress.md)。
|
||||||
|
|
||||||
- 已完成:T-000(正式代码包结构)、T-001(`app/editor.py` 模块化)、T-002(`app/appconfig.py` + `config.json`)、T-003(`app/db.py` + SQLite 建表)、T-004(本地数据 gitignore)、T-005(AI 模型清单后端)、T-006(单元测试基座)、T-101(账号 slug/user-data-dir)。
|
- 已完成:T-000(正式代码包结构)、T-001(`app/editor.py` 模块化)、T-002(`app/appconfig.py` + `config.json`)、T-003(`app/db.py` + SQLite 建表)、T-004(本地数据 gitignore)、T-005(AI 模型清单后端)、T-006(单元测试基座)、T-101(账号 slug/user-data-dir)、T-102(Chrome 启动器)。
|
||||||
- 正在进行:无。
|
- 正在进行:无。
|
||||||
- 下一个可领取任务:**T-102(`app/chrome.py` 启动器)**。
|
- 下一个可领取任务:**T-103(首次登录保活 + 登录检测)**。
|
||||||
|
|
||||||
## 当前可运行内容
|
## 当前可运行内容
|
||||||
|
|
||||||
@@ -78,6 +79,9 @@ py -3 -c "import os,tempfile; from app import appconfig; d=tempfile.TemporaryDir
|
|||||||
# config slug 与 user-data-dir 临时目录检查
|
# config slug 与 user-data-dir 临时目录检查
|
||||||
py -3 -c "import tempfile; from app import config; d=tempfile.TemporaryDirectory(dir='.'); print(config.ensure_user_data_dir(config.make_slug('alias'), root=d.name))"
|
py -3 -c "import tempfile; from app import config; d=tempfile.TemporaryDirectory(dir='.'); print(config.ensure_user_data_dir(config.make_slug('alias'), root=d.name))"
|
||||||
|
|
||||||
|
# chrome 参数拼装 / 端口探测由 tests/test_chrome.py 覆盖
|
||||||
|
python -m unittest discover -s tests
|
||||||
|
|
||||||
# 当前入口占位
|
# 当前入口占位
|
||||||
python main.py
|
python main.py
|
||||||
py -3 -m app
|
py -3 -m app
|
||||||
|
|||||||
@@ -303,3 +303,12 @@
|
|||||||
- 规则:slug = 可读 ASCII 前缀 + 8 位 SHA1 后缀;非 ASCII 别名使用 `account_<hash>`;`ensure_user_data_dir` 只接受 `[a-z0-9_]`,防止路径穿越。
|
- 规则:slug = 可读 ASCII 前缀 + 8 位 SHA1 后缀;非 ASCII 别名使用 `account_<hash>`;`ensure_user_data_dir` 只接受 `[a-z0-9_]`,防止路径穿越。
|
||||||
- 验证:`py -3 -m compileall app main.py tests` 通过;`py -3 -m unittest discover -s tests` 通过(12 tests,skipped=2);`python -m unittest discover -s tests` 通过(12 tests,skipped=2);测试临时目录已清理。
|
- 验证:`py -3 -m compileall app main.py tests` 通过;`py -3 -m unittest discover -s tests` 通过(12 tests,skipped=2);`python -m unittest discover -s tests` 通过(12 tests,skipped=2);测试临时目录已清理。
|
||||||
- 下一步:按任务看板领取 T-102。
|
- 下一步:按任务看板领取 T-102。
|
||||||
|
|
||||||
|
## 【2026-06-27】T-102 Chrome 启动器
|
||||||
|
|
||||||
|
- 状态:DONE
|
||||||
|
- 变更:新增 `app/chrome.py`,实现 `build_launch_args`、`launch_chrome`、`is_running`、`wait_debug_ready`;启动参数包含 `--remote-debugging-port`、`--remote-allow-origins=*`、`--user-data-dir`;新增 `tests/test_chrome.py` 覆盖参数拼装、Popen 调用模拟、本地 `/json/version` 端口探测;同步 `docs/06-tasks.md`、`docs/current-state.md`、`docs/api.md`。
|
||||||
|
- 细节:端口探测使用 stdlib `urllib` 访问 `127.0.0.1:<port>/json/version`,显式禁用环境代理,避免 localhost/CDP 被代理干扰。
|
||||||
|
- 范围:本轮不实现 `.lnk` 快捷方式,留给 T-106。
|
||||||
|
- 验证:`py -3 -m compileall app main.py tests` 通过;`py -3 -m unittest discover -s tests` 通过(17 tests,skipped=2);`python -m unittest discover -s tests` 通过(17 tests,skipped=2)。
|
||||||
|
- 下一步:按任务看板领取 T-103。
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import unittest
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
|
||||||
|
from _helpers import TempDirMixin
|
||||||
|
|
||||||
|
from app import chrome
|
||||||
|
from app import config as account_config
|
||||||
|
|
||||||
|
|
||||||
|
class JsonVersionHandler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path != "/json/version":
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
body = json.dumps({"Browser": "Chrome/Test"}).encode("utf-8")
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class ChromeTests(TempDirMixin, unittest.TestCase):
|
||||||
|
def start_json_version_server(self):
|
||||||
|
server = HTTPServer(("127.0.0.1", 0), JsonVersionHandler)
|
||||||
|
thread = threading.Thread(target=server.serve_forever)
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
self.addCleanup(server.server_close)
|
||||||
|
self.addCleanup(server.shutdown)
|
||||||
|
self.addCleanup(thread.join, 2)
|
||||||
|
return server.server_address[1]
|
||||||
|
|
||||||
|
def unused_port(self):
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.bind(("127.0.0.1", 0))
|
||||||
|
port = sock.getsockname()[1]
|
||||||
|
sock.close()
|
||||||
|
return port
|
||||||
|
|
||||||
|
def test_build_launch_args_includes_required_flags(self):
|
||||||
|
with self.make_temp_dir() as temp_dir:
|
||||||
|
cfg = {
|
||||||
|
"chrome_path": r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
||||||
|
"user_data_root": temp_dir,
|
||||||
|
}
|
||||||
|
account = {"alias": "alias", "debug_port": "9222"}
|
||||||
|
|
||||||
|
args = chrome.build_launch_args(account, config=cfg)
|
||||||
|
slug = account_config.make_slug("alias")
|
||||||
|
user_data_dir = os.path.abspath(os.path.join(temp_dir, slug))
|
||||||
|
|
||||||
|
self.assertEqual(cfg["chrome_path"], args[0])
|
||||||
|
self.assertIn("--remote-debugging-port=9222", args)
|
||||||
|
self.assertIn("--remote-allow-origins=*", args)
|
||||||
|
self.assertIn(f"--user-data-dir={user_data_dir}", args)
|
||||||
|
self.assertTrue(os.path.isdir(user_data_dir))
|
||||||
|
|
||||||
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
|
def test_build_launch_args_accepts_account_object_user_data_dir(self):
|
||||||
|
with self.make_temp_dir() as temp_dir:
|
||||||
|
user_data_dir = os.path.join(temp_dir, "custom")
|
||||||
|
account = SimpleNamespace(debug_port=9333, user_data_dir=user_data_dir)
|
||||||
|
args = chrome.build_launch_args(
|
||||||
|
account,
|
||||||
|
config={"chrome_path": "chrome.exe"},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("--remote-debugging-port=9333", args)
|
||||||
|
self.assertIn(f"--user-data-dir={os.path.abspath(user_data_dir)}", args)
|
||||||
|
self.assertTrue(os.path.isdir(user_data_dir))
|
||||||
|
|
||||||
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
|
def test_build_launch_args_rejects_invalid_account(self):
|
||||||
|
with self.assertRaises(chrome.ChromeLaunchError):
|
||||||
|
chrome.build_launch_args(
|
||||||
|
{"alias": "alias", "debug_port": 0},
|
||||||
|
config={"chrome_path": "chrome.exe"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_launch_chrome_uses_subprocess_popen(self):
|
||||||
|
with self.make_temp_dir() as temp_dir:
|
||||||
|
account = {"alias": "alias", "debug_port": 9222}
|
||||||
|
cfg = {"chrome_path": "chrome.exe", "user_data_root": temp_dir}
|
||||||
|
process = object()
|
||||||
|
|
||||||
|
with mock.patch("app.chrome.subprocess.Popen", return_value=process) as popen:
|
||||||
|
result = chrome.launch_chrome(account, config=cfg)
|
||||||
|
|
||||||
|
self.assertIs(process, result)
|
||||||
|
popen.assert_called_once()
|
||||||
|
args = popen.call_args[0][0]
|
||||||
|
self.assertEqual("chrome.exe", args[0])
|
||||||
|
self.assertIn("--remote-debugging-port=9222", args)
|
||||||
|
self.assertIn("--remote-allow-origins=*", args)
|
||||||
|
|
||||||
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
|
def test_is_running_and_wait_debug_ready(self):
|
||||||
|
port = self.start_json_version_server()
|
||||||
|
|
||||||
|
self.assertTrue(chrome.is_running(port))
|
||||||
|
self.assertTrue(chrome.wait_debug_ready(port, timeout=1))
|
||||||
|
self.assertFalse(chrome.is_running(self.unused_port()))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user