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()