Save and compare UI tree snapshots.
| 31 | |
| 32 | |
| 33 | class SnapshotStore: |
| 34 | """Save and compare UI tree snapshots.""" |
| 35 | |
| 36 | def __init__(self, snapshot_dir: Path = SNAPSHOT_DIR) -> None: |
| 37 | self._dir = snapshot_dir |
| 38 | |
| 39 | def _path(self, name: str) -> Path: |
| 40 | if not SNAPSHOT_NAME_RE.fullmatch(name): |
| 41 | msg = f"Invalid snapshot name: {name!r}" |
| 42 | raise ValueError(msg) |
| 43 | return self._dir / f"{name}.json" |
| 44 | |
| 45 | def save(self, name: str, data: dict[str, Any]) -> Path: |
| 46 | """Save a tree dump as a named snapshot.""" |
| 47 | self._dir.mkdir(parents=True, exist_ok=True) |
| 48 | path = self._path(name) |
| 49 | path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") |
| 50 | return path |
| 51 | |
| 52 | def load(self, name: str) -> dict[str, Any] | None: |
| 53 | """Load a saved snapshot. Returns None if not found.""" |
| 54 | path = self._path(name) |
| 55 | if not path.exists(): |
| 56 | return None |
| 57 | try: |
| 58 | result: dict[str, Any] = json.loads(path.read_text(encoding="utf-8")) |
| 59 | return result |
| 60 | except (json.JSONDecodeError, OSError): |
| 61 | return None |
| 62 | |
| 63 | def diff(self, name: str, current: dict[str, Any]) -> dict[str, Any]: |
| 64 | """Compare current tree against a saved snapshot.""" |
| 65 | baseline = self.load(name) |
| 66 | if baseline is None: |
| 67 | msg = f"Snapshot not found: {name!r}" |
| 68 | raise FileNotFoundError(msg) |
| 69 | |
| 70 | baseline_elements = _collect_elements(baseline) |
| 71 | current_elements = _collect_elements(current) |
| 72 | |
| 73 | return _compare_elements(baseline_elements, current_elements) |
| 74 | |
| 75 | def list_names(self) -> list[str]: |
| 76 | """List all saved snapshot names.""" |
| 77 | if not self._dir.exists(): |
| 78 | return [] |
| 79 | return sorted(p.stem for p in self._dir.glob("*.json")) |
| 80 | |
| 81 | def delete(self, name: str) -> bool: |
| 82 | """Delete a saved snapshot. Returns True if deleted.""" |
| 83 | path = self._path(name) |
| 84 | if path.exists(): |
| 85 | path.unlink() |
| 86 | return True |
| 87 | return False |
| 88 | |
| 89 | |
| 90 | def _collect_elements(data: dict[str, Any]) -> list[dict[str, Any]]: |
no outgoing calls