import json import subprocess import textwrap import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] SCRIPT_PATH = ( REPO_ROOT.parent / "skill_lib" / "skills" / "zhihu-navigate" / "scripts" / "open_creator_entry.js" ) def run_open_creator_entry(*, 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 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 node = {{ tagName: String(spec?.tagName || 'DIV').toUpperCase(), textContent: String(spec?.textContent ?? ''), innerText: String(spec?.innerText ?? spec?.textContent ?? ''), href: String(spec?.href ?? ''), clicked: false, click() {{ this.clicked = true; }}, getBoundingClientRect() {{ return {{ width: spec?.visible === false ? 0 : 120, height: spec?.visible === false ? 0 : 32, }}; }}, }}; 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: {{ desired_target: 'article_editor' }}, location: {{ href: 'https://www.zhihu.com/creator' }}, document: {{ body: bodyNode, querySelector(selector) {{ if (selector === 'body') {{ return bodyNode; }} return createNodeList(selector)[0] || null; }}, querySelectorAll(selector) {{ return createNodeList(selector); }}, }}, 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 SkillScriptZhihuNavigateTest(unittest.TestCase): def test_open_creator_entry_clicks_anchor_write_entry(self): payload = run_open_creator_entry( body_text="创作者中心 写文章", selectors={ "a[href], button, [role='button']": [ { "tagName": "a", "textContent": "写文章", "href": "https://zhuanlan.zhihu.com/write", } ] }, ) self.assertEqual(payload["result"]["status"], "creator_entry_clicked") self.assertTrue(payload["created"]["a[href], button, [role='button']#0"]["clicked"]) def test_open_creator_entry_clicks_button_write_entry(self): payload = run_open_creator_entry( body_text="创作者中心 发布内容", selectors={ "a[href], button, [role='button']": [ { "tagName": "button", "textContent": "写文章", } ] }, ) self.assertEqual(payload["result"]["status"], "creator_entry_clicked") self.assertTrue(payload["created"]["a[href], button, [role='button']#0"]["clicked"]) if __name__ == "__main__": unittest.main()