用户要求:"把你每一步操作都放出来让我自己写,你都封在一起我也不知道切号/认号的流程"
→ 把两个打包步骤的内部动作,拆成**画布上拖得出来的普通步骤**(能拆是因为补了下面两个零件)。
① 选择器值里的**变量**(tasks/generic/task.py `_vars_sub`)
`{本次要发的名字}`(循环块设的)/ `{本机账号名字}`(读名字读到的),中文别名与变量名都认。
用在 click / long_click / wait_el / swipe_until / expect_el / read_text / read_account_name /
switch_account 的选择器值,以及 if_el 的选择器与手填候选值。
· 没写花括号的步骤一个字不改(老任务零影响)
· 变量为空 → **换成空串 + 记警告**:宁可"选择器变空、找不到元素"明着报,
也不留 `{…}` 字面量去点到歪的地方
· 不做通用表达式(只认表里那几个名字)
② 「点击元素」的选择器类型下拉补上 **textContains**(文本包含)——
以前只能用打包步骤做到的"按名字点列表那一行",现在自己就能拼:
`textContains` + 值 `{本次要发的名字}`
③ 新步骤 **read_text「读取文字」**:把元素文字读出来存进指定变量
(`var`=account_name 时顺手把昵称控件记给切号/推送核对复用;`clean`=按账号名清洗/只去空白;
`on_missing`=skip 默认只记一句 / fail 算失败)。之前**没有**"读取元素文字"这种步骤。
④ 「条件判断」的候选值来源加 **「变量」**(`cmp_source=var` + `cmp_var`)——
不然 read_text 读出来的东西没处用。空变量 = 候选为空 = 不命中(同"读不到名字"的规矩)。
⑤ fix(发布) 实测踩出来的两处(A06,截图/日志为证):
· **剪辑页校验 12 → 45 秒**:点完「下一步」抖音在「正在导入」(一条 25 秒的视频要导 25 秒+),
导入期间工具栏不渲染,10~12 秒必然判"没到剪辑页"→ 这一条白发
· **切号前先回主界面**(`_goto_me`):点不到「我」页签 = 不在主界面(剪辑页/发布页),
那种页面没有底部页签 → **强杀 + 冷启动**抖音再来。实测卡在剪辑页时,本轮剩下的号全部报废
· **切号后回读为空时点一次「我」再读**:换号会整体重启抖音、起来常落在首页(首页没有昵称控件),
读多少次都是空 —— 等完 wait_after、读两遍仍空才补这一下导航,不打扰它的重启过程
前端:编辑器调色板/字段面板认 read_text(含变量说明)、选择器类型加 textContains、
placeholder 提示 `{本次要发的名字}`、候选值来源加「变量」+ 变量名输入、
「发布计划」页就地编辑字段表同步(release.js)。
文档:TASK_DEV §3 步骤表 28→29 种 + §3.3「选择器里的变量」+ §3.4「自己拼切号+认号」;
API.md 骨架步数修正(18 → 4 步 / 含嵌套 23 步)。
测试:新增 scripts/tests/test_read_text.py(变量代入、空变量不许瞎点、读取文字存变量/清洗、
条件判断取变量、草稿校验),全套 8 个用例通过。
199 lines
8.4 KiB
Python
199 lines
8.4 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""「读取文字」+ 选择器里的 `{变量}` +「条件判断→变量」用例。
|
||
|
||
假设备,不连真机。验三件事:
|
||
① 选择器值里写 `{本次要发的名字}` / `{本机账号名字}` → 真的换成本轮的值;
|
||
变量是空的 → 换成空串、这一步找不到元素(**不能留着字面量去瞎点**);
|
||
② 「读取文字」把元素文字读出来、清洗、存进指定变量(变量名叫 account_name 时顺手记下昵称控件);
|
||
③ 「条件判断」候选值来源选「变量」→ 候选就是那个变量的值,空变量 = 不命中。
|
||
"""
|
||
import io
|
||
import os
|
||
import sqlite3
|
||
import sys
|
||
import tempfile
|
||
|
||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||
os.environ['DATABASE_URL'] = 'sqlite:///' + os.path.join(tempfile.mkdtemp(), 'ac.db').replace('\\', '/')
|
||
os.environ['DATA_VIDEO_DIR'] = tempfile.mkdtemp()
|
||
os.environ['DEPLOY_ENV'] = 'dev'
|
||
os.environ['DB_ALLOW_SQLITE_FALLBACK'] = '1'
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||
os.chdir(sys.path[0])
|
||
|
||
from tasks.generic import task as gt # noqa: E402
|
||
from core import task_draft # noqa: E402
|
||
|
||
fails = []
|
||
|
||
|
||
def check(label, got, want):
|
||
ok = got == want
|
||
if not ok:
|
||
fails.append(label)
|
||
print((' ✅ ' if ok else ' ❌ ') + label + ('' if ok else '\n 得到 %r\n 期望 %r' % (got, want)))
|
||
|
||
|
||
class El:
|
||
def __init__(self, flag, text=""):
|
||
self._flag = flag
|
||
self._text = text
|
||
self.clicked = False
|
||
|
||
@property
|
||
def exists(self):
|
||
return _Ex(self._flag)
|
||
|
||
def wait(self, timeout=0):
|
||
return self._flag
|
||
|
||
def get_text(self, timeout=None):
|
||
return self._text
|
||
|
||
def click(self):
|
||
self.clicked = True
|
||
|
||
|
||
class _Ex:
|
||
def __init__(self, f):
|
||
self.f = f
|
||
|
||
def __call__(self, timeout=0):
|
||
return self.f
|
||
|
||
def __bool__(self):
|
||
return self.f
|
||
|
||
|
||
class StubD:
|
||
"""`kw` 里记最后一次 d(**kw) 的参数;`texts` 给元素文本。"""
|
||
|
||
def __init__(self, present=(), texts=None):
|
||
self.present = set(present)
|
||
self.texts = texts or {}
|
||
self.last_kw = None
|
||
self.clicked = []
|
||
|
||
def xpath(self, v):
|
||
return El(v in self.present, self.texts.get(v, ""))
|
||
|
||
def __call__(self, **kw):
|
||
self.last_kw = kw
|
||
v = list(kw.values())[0]
|
||
if v in self.present:
|
||
self.clicked.append(v)
|
||
return El(v in self.present, self.texts.get(v, ""))
|
||
|
||
|
||
print('=== 1. 选择器值里的 {本次要发的名字} 换成本轮目标 ===')
|
||
w = gt.GenericStepsWorker("F1", {})
|
||
w._vars['target_name'] = '锋哥苏州寻宝藏小店'
|
||
d = StubD(present=['锋哥苏州寻宝藏小店'])
|
||
check('点击命中(说明选择器已代入)',
|
||
w._exec_click(d, {"selector_type": "textContains",
|
||
"selector_value": "{本次要发的名字}", "wait_timeout": 1}, 0), True)
|
||
check('实际用的选择器 = 名字本身', d.last_kw, {'textContains': '锋哥苏州寻宝藏小店'})
|
||
|
||
print('=== 2. 变量还是空的 → 选择器变空 → 不许瞎点 ===')
|
||
w = gt.GenericStepsWorker("F2", {})
|
||
d = StubD(present=[''])
|
||
check('返回 False(没找到)',
|
||
w._exec_click(d, {"selector_type": "textContains",
|
||
"selector_value": "prefix-{本次要发的名字}", "wait_timeout": 1}, 0), False)
|
||
check('空变量 → 字符串里不留 {…} 字面量', d.last_kw, {'textContains': 'prefix-'})
|
||
|
||
print('=== 3. {本机账号名字} 用「读取文字」读到的值 ===')
|
||
w = gt.GenericStepsWorker("F3", {})
|
||
w._vars['account_name'] = '小王'
|
||
d = StubD(present=['小王'])
|
||
check('等待元素命中', w._exec_wait_el(d, {"selector_type": "text",
|
||
"selector_value": "{本机账号名字}", "timeout": 1}, 0), True)
|
||
check('用的是本机账号名', d.last_kw, {'text': '小王'})
|
||
|
||
print('=== 4. 「读取文字」读出来、清洗、存变量 ===')
|
||
w = gt.GenericStepsWorker("F4", {})
|
||
d = StubD(present=['//*[@text="nick"]'],
|
||
texts={'//*[@text="nick"]': ' 锋哥苏州康养探店 \n编辑资料 \n抖音号:123'})
|
||
check('返回 True', w._exec_read_text(d, {"selector_type": "xpath",
|
||
"selector_value": '//*[@text="nick"]',
|
||
"var": "account_name", "clean": "account",
|
||
"timeout": 1}, 0), True)
|
||
check('存进 account_name(按账号名清洗:取第一行非标签)',
|
||
w._vars.get('account_name'), '锋哥苏州康养探店')
|
||
check('顺手记下昵称控件(切号/推送核对复用)',
|
||
w._vars.get('name_selector'), ('xpath', '//*[@text="nick"]'))
|
||
|
||
print('=== 5. clean=strip:原样(只去首尾空白) ===')
|
||
w = gt.GenericStepsWorker("F5", {})
|
||
d = StubD(present=['//*[@text="x"]'],
|
||
texts={'//*[@text="x"]': ' 小王 \n编辑资料'})
|
||
w._exec_read_text(d, {"selector_type": "xpath", "selector_value": '//*[@text="x"]',
|
||
"var": "raw_name", "clean": "strip", "timeout": 1}, 0)
|
||
check('只去首尾空白、不按行取', w._vars.get('raw_name'), '小王 \n编辑资料')
|
||
check('变量名不是 account_name → 不记昵称控件', w._vars.get('name_selector'), None)
|
||
|
||
print('=== 6. 读不到:默认 skip(只记一句、不拦任务) ===')
|
||
w = gt.GenericStepsWorker("F6", {})
|
||
d = StubD(present=[])
|
||
check('返回 None(skip 不算失败)',
|
||
w._exec_read_text(d, {"selector_type": "xpath", "selector_value": '//*[@text="没有"]',
|
||
"var": "account_name", "timeout": 1}, 0), None)
|
||
check('变量被置空', w._vars.get('account_name'), '')
|
||
|
||
print('=== 7. 读不到 + on_missing=fail → 算这一步失败 ===')
|
||
w = gt.GenericStepsWorker("F7", {})
|
||
d = StubD(present=[])
|
||
check('返回 False',
|
||
w._exec_read_text(d, {"selector_type": "xpath", "selector_value": '//*[@text="没有"]',
|
||
"var": "v", "timeout": 1, "on_missing": "fail"}, 0), False)
|
||
|
||
print('=== 8. 「条件判断」候选值来源=变量 ===')
|
||
w = gt.GenericStepsWorker("F8", {})
|
||
w._vars['account_name'] = '小王'
|
||
val, note = w._resolve_cmp_value({"cmp_op": "等于", "cmp_source": "var",
|
||
"cmp_var": "account_name"}, "")
|
||
check('候选值 = 变量的值', val, '小王')
|
||
check('来源说明写清了', 'account_name' in note and '小王' in note, True)
|
||
|
||
print('=== 9. 变量是空的 → 候选为空(永远不命中,走 else) ===')
|
||
w = gt.GenericStepsWorker("F9", {})
|
||
val, note = w._resolve_cmp_value({"cmp_op": "等于", "cmp_source": "var",
|
||
"cmp_var": "account_name"}, "手填兜底")
|
||
check('只剩手填值', val, '手填兜底')
|
||
check('说明里点出变量是空的', '空' in note, True)
|
||
|
||
print('=== 10. 草稿校验认这两个新能力 ===')
|
||
ok_steps = [{"type": "read_text", "label": "读取文字",
|
||
"params": {"selector_type": "xpath", "selector_value": '//*[@id="nick"]',
|
||
"var": "account_name"}},
|
||
{"type": "if_el", "label": "条件判断",
|
||
"params": {"selector_type": "xpath", "selector_value": '//*[@id="nick"]',
|
||
"cmp_op": "包含", "cmp_source": "var", "cmp_var": "account_name",
|
||
"then": [{"type": "notify", "label": "命中",
|
||
"params": {"title": "ok", "message": "ok"}}],
|
||
"else": []}}]
|
||
errs, warns = [], []
|
||
task_draft.validate_steps(ok_steps, errors=errs, warnings=warns)
|
||
check('合法草稿没有 error', errs, [])
|
||
|
||
bad_steps = [{"type": "read_text", "label": "读取文字",
|
||
"params": {"selector_type": "xpath", "selector_value": ""}}]
|
||
errs2 = []
|
||
task_draft.validate_steps(bad_steps, errors=errs2, warnings=[])
|
||
check('「读取文字」缺选择器 → 报错',
|
||
any('selector_value' in e for e in errs2), True)
|
||
|
||
bad2 = [{"type": "if_el", "label": "条件判断",
|
||
"params": {"selector_type": "xpath", "selector_value": '//*[@id="x"]',
|
||
"cmp_op": "包含", "cmp_source": "var", "then": [], "else": []}}]
|
||
errs3 = []
|
||
task_draft.validate_steps(bad2, errors=errs3, warnings=[])
|
||
check('来源=变量但没填变量名 → 报错',
|
||
any('cmp_var' in e for e in errs3), True)
|
||
|
||
print()
|
||
if fails:
|
||
print('❌ 失败 %d 项:%s' % (len(fails), ' / '.join(fails)))
|
||
sys.exit(1)
|
||
print('✅ 全部通过(没连任何真机)')
|