import json import base64 import struct from pathlib import Path import sys import tempfile import threading import unittest from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import urlsplit sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from ui import Inspector, Phone, save_png, imported_watch_capture, jpeg_info, health_screenshot class UIHandler(BaseHTTPRequestHandler): def log_message(self, *_): pass def do_GET(self): self.server.paths.append(self.path) if self.path == '/snapshot.json': value = {'frames': [{'snapshot': 'frame.png'}]} body = json.dumps(value).encode() else: body = b'broken PNG' self.send_response(200) self.send_header('Content-Length', str(len(body))) self.end_headers() self.wfile.write(body) def do_POST(self): self.server.paths.append(self.path) body = b'{}' self.send_response(200) self.send_header('Content-Length', str(len(body))) self.end_headers() self.wfile.write(body) class UiTests(unittest.TestCase): @staticmethod def jpeg_header(width=480, height=464): # Minimal marker stream exercises metadata parsing, not a JPEG decoder. return b'\xff\xd8\xff\xc0' + struct.pack('>HBHHB', 8, 8, height, width, 0) + b'\xff\xd9' def test_watch_jpeg_dimensions_and_truncation(self): data = self.jpeg_header() self.assertEqual((jpeg_info(data)['width'], jpeg_info(data)['height']), (480, 464)) with self.assertRaises(ValueError): jpeg_info(data[:-1]) def test_official_numbered_jpeg_is_exported_with_real_extension(self): data = self.jpeg_header() class HealthPhone: queries = 0 find = staticmethod(Phone.find) def unlocked(self): pass def click(self, selector): pass def tree(self): return '' def run(self, *args, binary=False): if binary: self.path = args[-1] return data if 'content query' in args[-1]: self.queries += 1 return '' if self.queries == 1 else 'Row: 0 _id=9, _data=/storage/emulated/0/Pictures/com.heytap.health/screenshots/watch_screen_shot (2).jpeg, width=480, height=464, date_modified=100' return '' phone = HealthPhone() with tempfile.TemporaryDirectory() as tmp: result = health_screenshot(phone, Path(tmp) / 'watch.png') self.assertEqual(Path(result['file']).suffix, '.jpg') self.assertEqual(Path(result['file']).read_bytes(), data) self.assertTrue(phone.path.endswith('watch_screen_shot (2).jpeg')) def test_failed_inspector_image_always_resumes_runtime(self): server = ThreadingHTTPServer(('127.0.0.1', 0), UIHandler) server.paths = [] thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() try: client = Inspector('http://127.0.0.1:' + str(server.server_port)) with tempfile.TemporaryDirectory() as tmp: dest = Path(tmp) / 'capture.png' with self.assertRaises(ValueError): client.screenshot(dest) self.assertFalse(dest.exists()) self.assertEqual(server.paths[-1], '/control?pause=false') finally: server.shutdown(); server.server_close(); thread.join() def test_phone_ambiguous_selector_is_rejected(self): xml = '' with self.assertRaises(ValueError): Phone.find(xml, ('text', '安装')) def test_phone_hidden_node_is_not_selected(self): self.assertIsNone(Phone.find('', ('text', '隐藏'))) self.assertIsNone(Phone.find('', ('text', '隐藏'))) def test_import_integrity_failure_does_not_write_capture(self): class ImportedPhone: def run(self, *args, binary=False): return base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aL1sAAAAASUVORK5CYII=') if binary else '{"sha256":"wrong"}' with tempfile.TemporaryDirectory() as tmp: dest = Path(tmp) / 'watch.png' with self.assertRaises(ValueError): imported_watch_capture(ImportedPhone(), dest) self.assertFalse(dest.exists()) def test_inspector_is_explicit_loopback(self): for url in ('https://example.com', 'http://10.0.0.1', 'http://user:secret@localhost'): with self.assertRaises(ValueError): Inspector(url) def test_no_phone_image_fallback_on_capture_failure(self): with tempfile.TemporaryDirectory() as tmp: dest = Path(tmp) / 'watch.png' with self.assertRaises(ValueError): save_png(dest, b'failure') self.assertFalse(dest.exists()) if __name__ == '__main__': unittest.main()