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 = {{ id: String(spec?.id ?? ''), tagName: String(spec?.tagName || 'DIV').toUpperCase(), textContent: String(spec?.textContent ?? ''), innerText: String(spec?.innerText ?? spec?.textContent ?? ''), href: String(spec?.href ?? ''), className: String(spec?.className ?? ''), role: String(spec?.role ?? ''), tabIndex: Number.isFinite(spec?.tabIndex) ? Number(spec.tabIndex) : -1, clicked: false, click() {{ this.clicked = true; }}, getAttribute(name) {{ if (name === 'class') {{ return this.className; }} if (name === 'role') {{ return this.role; }} if (name === 'href') {{ return this.href; }} if (name === 'tabindex' && this.tabIndex >= 0) {{ return String(this.tabIndex); }} return ''; }}, getBoundingClientRect() {{ return {{ width: spec?.visible === false ? 0 : 120, height: spec?.visible === false ? 0 : 32, }}; }}, parentElement: null, closest(selector) {{ let current = this; while (current) {{ if (matchesSelector(current, selector)) {{ return current; }} current = current.parentElement; }} return null; }}, }}; return node; }} const created = new Map(); const nodesById = new Map(); function matchesSelector(node, selector) {{ const parts = selector.split(',').map((part) => part.trim()).filter(Boolean); return parts.some((part) => {{ if (part === 'a[href]') {{ return node.tagName === 'A' && Boolean(node.href); }} if (part === 'button') {{ return node.tagName === 'BUTTON'; }} if (part === '[role=\"button\"]' || part === "[role='button']") {{ return node.role === 'button'; }} if (part === '[tabindex]') {{ return node.tabIndex >= 0; }} if (part === 'div') {{ return node.tagName === 'DIV'; }} if (part === 'span') {{ return node.tagName === 'SPAN'; }} return false; }}); }} function createNodeList(selector) {{ const specs = selectorMap[selector] || []; return specs.map((spec, index) => {{ const key = `${{selector}}#${{index}}`; if (!created.has(key)) {{ const node = createNode(spec); created.set(key, node); if (node.id) {{ nodesById.set(node.id, node); }} }} return created.get(key); }}); }} for (const selector of Object.keys(selectorMap)) {{ createNodeList(selector); }} for (const node of created.values()) {{ const parentId = selectorMap && Object.values(selectorMap) .flat() .find((spec) => String(spec?.id ?? '') === node.id)?.parentId; if (parentId && nodesById.has(parentId)) {{ node.parentElement = nodesById.get(parentId); }} }} 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"]) def test_open_creator_entry_clicks_clickable_ancestor_for_nested_write_text(self): payload = run_open_creator_entry( body_text="创作者中心 写文章", selectors={ "a[href], button, [role='button']": [], "div, span, [tabindex]": [ { "id": "ancestor", "tagName": "div", "className": "creator-entry", "tabIndex": 0, "textContent": "", }, { "id": "label", "parentId": "ancestor", "tagName": "span", "textContent": "写文章", }, ], }, ) self.assertEqual(payload["result"]["status"], "creator_entry_clicked") self.assertTrue(payload["created"]["div, span, [tabindex]#0"]["clicked"]) if __name__ == "__main__": unittest.main()