fix(client): wait for PDD foreground after intent

This commit is contained in:
QiuSW
2026-08-04 09:40:05 +08:00
parent 9452debf66
commit cb646b4974
4 changed files with 126 additions and 16 deletions
+84 -4
View File
@@ -63,11 +63,13 @@ class FakeUiDevice:
*,
version: str = "8.17.0",
current_package: str = "com.xunmeng.pinduoduo",
current_packages: list[str] | None = None,
hierarchy: str = HIERARCHY,
timeout_on_screenshot: bool = False,
) -> None:
self.version = version
self.current_package = current_package
self.current_packages = list(current_packages) if current_packages is not None else None
self.hierarchy = hierarchy
self.timeout_on_screenshot = timeout_on_screenshot
self.calls: list[str] = []
@@ -78,6 +80,9 @@ class FakeUiDevice:
def app_current(self) -> dict[str, str]:
self.calls.append("app_current")
if self.current_packages:
package = self.current_packages.pop(0)
self.current_package = package
return {"package": self.current_package, "activity": "sensitive.activity.name"}
def jsonrpc_call(self, method: str, params: object = None, timeout: float = 10) -> str:
@@ -92,8 +97,8 @@ class FakeUiDevice:
class ProductOpenTests(unittest.TestCase):
def _capturer(self, adb: FakeAdbClient, device: FakeUiDevice) -> ProductOpenCapturer:
return ProductOpenCapturer(adb, lambda serial: device, timeout_seconds=2)
def _capturer(self, adb: FakeAdbClient, device: FakeUiDevice, **kwargs: object) -> ProductOpenCapturer:
return ProductOpenCapturer(adb, lambda serial: device, timeout_seconds=2, **kwargs)
def test_success_uses_canonical_url_and_redacted_atomic_manifest(self) -> None:
adb = FakeAdbClient()
@@ -139,15 +144,77 @@ class ProductOpenTests(unittest.TestCase):
def test_foreground_package_mismatch_halts_before_capture(self) -> None:
adb = FakeAdbClient()
device = FakeUiDevice(current_package="com.example.other")
clock = FakeClock()
with TemporaryDirectory() as temporary:
target = Path(temporary) / "evidence"
with self.assertRaises(ProductPackageMismatchError):
self._capturer(adb, device).open_and_capture(SERIAL, URL, target)
self._capturer(
adb,
device,
foreground_poll_interval_seconds=0.5,
monotonic_clock=clock.monotonic,
sleep_function=clock.sleep,
).open_and_capture(SERIAL, URL, target)
self.assertEqual(adb.calls, [("inspect", SERIAL), ("intent", "123")])
self.assertEqual(device.calls, ["app_info", "app_current"])
self.assertEqual(device.calls, ["app_info", "app_current", "app_current", "app_current", "app_current", "app_current"])
self.assertEqual(clock.sleeps, [0.5, 0.5, 0.5, 0.5])
self.assertFalse(target.exists())
def test_foreground_package_poll_waits_for_pdd_before_reading_evidence(self) -> None:
adb = FakeAdbClient()
device = FakeUiDevice(current_packages=["com.example.other", "com.xunmeng.pinduoduo"])
clock = FakeClock()
with TemporaryDirectory() as temporary:
target = Path(temporary) / "evidence"
result = self._capturer(
adb,
device,
foreground_poll_interval_seconds=0.25,
monotonic_clock=clock.monotonic,
sleep_function=clock.sleep,
).open_and_capture(SERIAL, URL, target)
self.assertTrue(result.manifest_path.exists())
self.assertEqual(clock.sleeps, [0.25])
self.assertEqual(
device.calls,
["app_info", "app_current", "app_current", "takeScreenshot", "dumpWindowHierarchy"],
)
def test_foreground_package_poll_stops_at_deadline_without_evidence(self) -> None:
adb = FakeAdbClient()
device = FakeUiDevice(current_packages=["com.example.other", "", "com.example.other"])
clock = FakeClock()
with TemporaryDirectory() as temporary:
target = Path(temporary) / "evidence"
with self.assertRaises(ProductPackageMismatchError):
self._capturer(
adb,
device,
foreground_poll_interval_seconds=0.8,
monotonic_clock=clock.monotonic,
sleep_function=clock.sleep,
).open_and_capture(SERIAL, URL, target)
self.assertEqual(len(clock.sleeps), 3)
for actual, expected in zip(clock.sleeps, (0.8, 0.8, 0.4), strict=True):
self.assertAlmostEqual(actual, expected)
self.assertEqual(device.calls, ["app_info", "app_current", "app_current", "app_current", "app_current"])
self.assertFalse(target.exists())
self.assertEqual(list(Path(temporary).glob(".evidence.staging-*")), [])
def test_foreground_poll_interval_must_be_positive_and_finite(self) -> None:
for interval in (0, -0.1, float("inf"), float("nan"), True):
with self.subTest(interval=interval):
with self.assertRaises(ValueError):
ProductOpenCapturer(
FakeAdbClient(),
lambda serial: FakeUiDevice(),
timeout_seconds=2,
foreground_poll_interval_seconds=interval, # type: ignore[arg-type]
)
def test_timeout_and_invalid_hierarchy_leave_no_partial_evidence(self) -> None:
scenarios = (
(FakeUiDevice(timeout_on_screenshot=True), ProductOpenTimeoutError),
@@ -184,3 +251,16 @@ class ProductOpenTests(unittest.TestCase):
self.assertTrue(forbidden.isdisjoint(ProductOpenUiDevice.__dict__))
self.assertEqual(base64.b64decode(_png_base64())[:8], b"\x89PNG\r\n\x1a\n")
class FakeClock:
def __init__(self) -> None:
self.value = 0.0
self.sleeps: list[float] = []
def monotonic(self) -> float:
return self.value
def sleep(self, seconds: float) -> None:
self.sleeps.append(seconds)
self.value += seconds