"""chart_shot.py 与 cast_chart --shot:HTML → PNG 截图的分级回退与优雅失败。""" import subprocess import sys import tempfile import unittest from pathlib import Path from unittest import mock SKILL_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(SKILL_ROOT / "scripts")) import chart_shot # noqa: E402 CHROME_OK = Path(chart_shot.CHROME).is_file() try: import playwright # noqa: F401 PLAYWRIGHT_OK = True except ImportError: PLAYWRIGHT_OK = False SHOT_OK = CHROME_OK or PLAYWRIGHT_OK HTML = "

命盘

" class ShootUnitTest(unittest.TestCase): def test_missing_html_exit_2(self): r = subprocess.run( [sys.executable, str(SKILL_ROOT / "scripts" / "chart_shot.py"), "/nonexistent/x.html"], capture_output=True, text=True, timeout=30) self.assertEqual(r.returncode, 2) self.assertIn("ERROR", r.stderr) def test_no_tools_fails_gracefully(self): """playwright 与 Chrome 都不可用时:shoot 返回 None,CLI 退出码 2 并给提示。""" with tempfile.TemporaryDirectory() as td: html = Path(td) / "p.html" html.write_text(HTML, encoding="utf-8") with mock.patch.object(chart_shot, "_shot_playwright", return_value=False), \ mock.patch.object(chart_shot, "_shot_chrome", return_value=False): self.assertIsNone(chart_shot.shoot(html)) self.assertFalse(html.with_suffix(".png").exists()) with mock.patch.object(sys, "argv", ["chart_shot.py", str(html)]): with mock.patch("sys.stderr") as err: self.assertEqual(chart_shot.main(), 2) err.write.assert_called() def test_content_height_bounds(self): """高度估算不越界:空页夹到下限,超长页夹到上限。""" with tempfile.TemporaryDirectory() as td: empty = Path(td) / "e.html" empty.write_text(HTML, encoding="utf-8") self.assertEqual(chart_shot._content_height(empty), 1300) big = Path(td) / "b.html" big.write_text( '
' + '
' * 5000 + "
", encoding="utf-8") self.assertEqual(chart_shot._content_height(big), 4200) @unittest.skipUnless(SHOT_OK, "环境无 playwright 与 Chrome,跳过真实截图") class ShootRealTest(unittest.TestCase): def test_real_png(self): """真实截图:PNG 非空、宽 2200(1100 窗口 × 2 倍分辨率)。""" import struct with tempfile.TemporaryDirectory() as td: html = Path(td) / "p.html" html.write_text(HTML, encoding="utf-8") png = chart_shot.shoot(html) self.assertIsNotNone(png) data = png.read_bytes() self.assertGreater(len(data), 1000) self.assertEqual(data[:8], b"\x89PNG\r\n\x1a\n") w, _h = struct.unpack(">II", data[16:24]) self.assertEqual(w, 2200) def test_cli_success(self): with tempfile.TemporaryDirectory() as td: html = Path(td) / "p.html" html.write_text(HTML, encoding="utf-8") out = Path(td) / "o.png" r = subprocess.run( [sys.executable, str(SKILL_ROOT / "scripts" / "chart_shot.py"), str(html), "-o", str(out)], capture_output=True, text=True, timeout=120) self.assertEqual(r.returncode, 0, r.stderr) self.assertIn(str(out), r.stdout) self.assertGreater(out.stat().st_size, 0) @unittest.skipUnless(SHOT_OK, "环境无 playwright 与 Chrome,跳过 --shot 端到端") class CastShotE2ETest(unittest.TestCase): def test_shot_flag_outputs_paths(self): with tempfile.TemporaryDirectory() as td: html = Path(td) / "p.html" r = subprocess.run( [sys.executable, str(SKILL_ROOT / "scripts" / "cast_chart.py"), "--name", "张三", "--gender", "男", "--birth", "1990-05-14 08:30", "--place", "四川省成都市", "--html", str(html), "--shot"], capture_output=True, text=True, timeout=120, cwd=str(SKILL_ROOT / "scripts")) self.assertEqual(r.returncode, 0, r.stderr) self.assertTrue(r.stdout.startswith("# 命盘:张三")) self.assertIn(f"HTML 盘面:{html.resolve()}", r.stdout) png = html.with_suffix(".png") self.assertIn(f"盘面截图:{png.resolve()}", r.stdout) self.assertGreater(png.stat().st_size, 0) if __name__ == "__main__": unittest.main()