sgclaw: snapshot today's runtime and skill updates
This commit is contained in:
219
tests/skill_script_zhihu_write_test.py
Normal file
219
tests/skill_script_zhihu_write_test.py
Normal file
@@ -0,0 +1,219 @@
|
||||
import json
|
||||
import subprocess
|
||||
import textwrap
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
PREPARE_SCRIPT_PATH = (
|
||||
REPO_ROOT.parent / "skill_lib" / "skills" / "zhihu-write" / "scripts" /
|
||||
"prepare_article_editor.js"
|
||||
)
|
||||
FILL_SCRIPT_PATH = (
|
||||
REPO_ROOT.parent / "skill_lib" / "skills" / "zhihu-write" / "scripts" /
|
||||
"fill_article_draft.js"
|
||||
)
|
||||
|
||||
|
||||
def run_browser_script(script_path: Path, *, args: dict, body_text: str, selectors: dict[str, list[dict]]) -> dict:
|
||||
node_script = textwrap.dedent(
|
||||
f"""
|
||||
import fs from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const scriptPath = {json.dumps(str(script_path))};
|
||||
const args = {json.dumps(args, ensure_ascii=False)};
|
||||
const selectorMap = {json.dumps(selectors, ensure_ascii=False)};
|
||||
const bodyText = {json.dumps(body_text, ensure_ascii=False)};
|
||||
const source = fs.readFileSync(scriptPath, 'utf8');
|
||||
|
||||
function createNode(spec) {{
|
||||
const attrs = spec?.attrs || {{}};
|
||||
const node = {{
|
||||
tagName: String(spec?.tagName || 'DIV').toUpperCase(),
|
||||
textContent: String(spec?.textContent ?? ''),
|
||||
innerText: String(spec?.innerText ?? spec?.textContent ?? ''),
|
||||
innerHTML: String(spec?.innerHTML ?? spec?.textContent ?? ''),
|
||||
value: String(spec?.value ?? ''),
|
||||
children: [],
|
||||
focused: false,
|
||||
clicked: false,
|
||||
appendChild(child) {{
|
||||
this.children.push(child);
|
||||
return child;
|
||||
}},
|
||||
focus() {{
|
||||
this.focused = true;
|
||||
}},
|
||||
click() {{
|
||||
this.clicked = true;
|
||||
}},
|
||||
dispatchEvent() {{
|
||||
return true;
|
||||
}},
|
||||
getAttribute(name) {{
|
||||
return Object.prototype.hasOwnProperty.call(attrs, name) ? attrs[name] : null;
|
||||
}},
|
||||
querySelector() {{
|
||||
return null;
|
||||
}},
|
||||
querySelectorAll() {{
|
||||
return [];
|
||||
}},
|
||||
getBoundingClientRect() {{
|
||||
return {{
|
||||
width: spec?.visible === false ? 0 : 100,
|
||||
height: spec?.visible === false ? 0 : 20,
|
||||
}};
|
||||
}},
|
||||
}};
|
||||
return node;
|
||||
}}
|
||||
|
||||
const created = new Map();
|
||||
|
||||
function createNodeList(selector) {{
|
||||
const specs = selectorMap[selector] || [];
|
||||
return specs.map((spec, index) => {{
|
||||
const key = `${{selector}}#${{index}}`;
|
||||
if (!created.has(key)) {{
|
||||
created.set(key, createNode(spec));
|
||||
}}
|
||||
return created.get(key);
|
||||
}});
|
||||
}}
|
||||
|
||||
const bodyNode = createNode({{ tagName: 'body', textContent: bodyText, innerText: bodyText }});
|
||||
const context = {{
|
||||
args,
|
||||
location: {{ href: 'https://zhuanlan.zhihu.com/write' }},
|
||||
document: {{
|
||||
body: bodyNode,
|
||||
createElement(tagName) {{
|
||||
return createNode({{ tagName }});
|
||||
}},
|
||||
createTextNode(text) {{
|
||||
return createNode({{ tagName: '#text', textContent: text, innerText: text }});
|
||||
}},
|
||||
querySelector(selector) {{
|
||||
if (selector === 'body') {{
|
||||
return bodyNode;
|
||||
}}
|
||||
return createNodeList(selector)[0] || null;
|
||||
}},
|
||||
querySelectorAll(selector) {{
|
||||
return createNodeList(selector);
|
||||
}},
|
||||
}},
|
||||
Event: class Event {{
|
||||
constructor(type, init = {{}}) {{
|
||||
this.type = type;
|
||||
this.bubbles = !!init.bubbles;
|
||||
this.composed = !!init.composed;
|
||||
}}
|
||||
}},
|
||||
console,
|
||||
JSON,
|
||||
Math,
|
||||
Number,
|
||||
Object,
|
||||
RegExp,
|
||||
Set,
|
||||
String,
|
||||
Array,
|
||||
Error,
|
||||
}};
|
||||
|
||||
try {{
|
||||
const result = vm.runInNewContext(`(function(){{\\n${{source}}\\n}})()`, context);
|
||||
process.stdout.write(JSON.stringify({{ ok: true, result, created: Object.fromEntries(created) }}));
|
||||
}} catch (error) {{
|
||||
process.stdout.write(JSON.stringify({{
|
||||
ok: false,
|
||||
error: String(error && error.message ? error.message : error),
|
||||
}}));
|
||||
process.exitCode = 1;
|
||||
}}
|
||||
"""
|
||||
)
|
||||
completed = subprocess.run(
|
||||
["node", "--input-type=module", "-e", node_script],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
payload = json.loads(completed.stdout)
|
||||
if completed.returncode != 0:
|
||||
raise AssertionError(payload["error"])
|
||||
return payload
|
||||
|
||||
|
||||
class SkillScriptZhihuWriteTest(unittest.TestCase):
|
||||
def test_prepare_article_editor_accepts_role_textbox_title_and_generic_body_editor(self):
|
||||
payload = run_browser_script(
|
||||
PREPARE_SCRIPT_PATH,
|
||||
args={"desired_mode": "draft"},
|
||||
body_text="写文章 发布",
|
||||
selectors={
|
||||
"[role='textbox'][aria-label*='标题']": [
|
||||
{
|
||||
"tagName": "div",
|
||||
"attrs": {
|
||||
"role": "textbox",
|
||||
"aria-label": "标题",
|
||||
"contenteditable": "true",
|
||||
},
|
||||
}
|
||||
],
|
||||
"div[contenteditable='true']": [
|
||||
{
|
||||
"tagName": "div",
|
||||
"attrs": {
|
||||
"contenteditable": "true",
|
||||
"data-placeholder": "在这里输入正文",
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(payload["result"]["status"], "editor_ready")
|
||||
|
||||
def test_fill_article_draft_accepts_role_textbox_title_and_generic_body_editor(self):
|
||||
payload = run_browser_script(
|
||||
FILL_SCRIPT_PATH,
|
||||
args={
|
||||
"title": "测试标题",
|
||||
"body": "第一段\n第二段",
|
||||
"publish_mode": "false",
|
||||
},
|
||||
body_text="写文章 发布",
|
||||
selectors={
|
||||
"[role='textbox'][aria-label*='标题']": [
|
||||
{
|
||||
"tagName": "div",
|
||||
"attrs": {
|
||||
"role": "textbox",
|
||||
"aria-label": "标题",
|
||||
"contenteditable": "true",
|
||||
},
|
||||
}
|
||||
],
|
||||
"div[contenteditable='true']": [
|
||||
{
|
||||
"tagName": "div",
|
||||
"attrs": {
|
||||
"contenteditable": "true",
|
||||
"data-placeholder": "在这里输入正文",
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(payload["result"]["status"], "draft_ready")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user