"""APIClient - Agent - Collection Runner dialog.""" from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTreeWidget, QTreeWidgetItem, QComboBox, QHeaderView, QProgressBar, QWidget ) from PyQt6.QtCore import QThread, pyqtSignal, Qt from PyQt6.QtGui import QBrush, QColor from app.ui.theme import Colors from app.core import storage, http_client from app.core.test_runner import run_tests from app.models import HttpRequest, CollectionRunResult class RunnerWorker(QThread): result_ready = pyqtSignal(object) finished = pyqtSignal() def __init__(self, requests: list[dict], variables: dict): super().__init__() self.requests = requests self.variables = variables def run(self): for r in self.requests: req = HttpRequest( method = r.get("method") or "GET", url = r.get("url") or "", headers = r.get("headers") or {}, params = r.get("params") or {}, body = r.get("body") or "", body_type = r.get("body_type") or "raw", auth_type = r.get("auth_type") or "none", auth_data = r.get("auth_data") or {}, test_script = r.get("test_script") or "", name = r.get("name") or r.get("url", ""), timeout = r.get("timeout") or 30, ssl_verify = bool(r.get("ssl_verify", 1)), ) resp = http_client.send_request(req, self.variables) test_results = run_tests(req.test_script, resp) result = CollectionRunResult( request_name = req.name or req.url, method = req.method, url = req.url, status = resp.status, elapsed_ms = resp.elapsed_ms, test_results = test_results, error = resp.error, ) self.result_ready.emit(result) self.finished.emit() class CollectionRunnerDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Collection Runner") self.setMinimumSize(800, 550) self._worker = None layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # ── Header ──────────────────────────────────────────────────────────── header = QWidget() header.setObjectName("panelHeader") header.setFixedHeight(52) hl = QHBoxLayout(header) hl.setContentsMargins(16, 0, 16, 0) title = QLabel("Collection Runner") title.setObjectName("panelTitle") hl.addWidget(title) hl.addStretch() col_label = QLabel("Collection:") col_label.setObjectName("fieldLabel") self.col_combo = QComboBox() self.col_combo.setMinimumWidth(200) self._collections = storage.get_collections() for c in self._collections: self.col_combo.addItem(c["name"], c["id"]) self.run_btn = QPushButton("Run All") self.run_btn.setObjectName("accent") self.run_btn.setFixedWidth(100) self.run_btn.clicked.connect(self._run) hl.addWidget(col_label) hl.addWidget(self.col_combo) hl.addSpacing(8) hl.addWidget(self.run_btn) layout.addWidget(header) # ── Body ────────────────────────────────────────────────────────────── body = QWidget() body.setObjectName("panelBody") bl = QVBoxLayout(body) bl.setContentsMargins(16, 12, 16, 12) self.progress = QProgressBar() self.progress.setValue(0) bl.addWidget(self.progress) self.result_tree = QTreeWidget() self.result_tree.setHeaderLabels(["Request", "Status", "Time", "Tests"]) self.result_tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) self.result_tree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) self.result_tree.header().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) self.result_tree.header().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) bl.addWidget(self.result_tree) self.summary_label = QLabel("") self.summary_label.setObjectName("fieldLabel") bl.addWidget(self.summary_label) layout.addWidget(body, 1) # ── Footer ──────────────────────────────────────────────────────────── footer = QWidget() footer.setObjectName("panelFooter") footer.setFixedHeight(52) fl = QHBoxLayout(footer) fl.setContentsMargins(16, 0, 16, 0) fl.addStretch() close_btn = QPushButton("Close") close_btn.setFixedWidth(80) close_btn.clicked.connect(self.accept) fl.addWidget(close_btn) layout.addWidget(footer) def _run(self): col_id = self.col_combo.currentData() if col_id is None: return requests = storage.get_all_requests(col_id) if not requests: self.summary_label.setText("No requests in this collection.") return self.result_tree.clear() self.progress.setMaximum(len(requests)) self.progress.setValue(0) self.run_btn.setEnabled(False) self._done = 0 self._passed_tests = 0 self._total_tests = 0 env = storage.get_active_environment() variables = env.variables if env else {} self._worker = RunnerWorker(requests, variables) self._worker.result_ready.connect(self._on_result) self._worker.finished.connect(self._on_finished) self._worker.start() def _on_result(self, result: CollectionRunResult): self._done += 1 self.progress.setValue(self._done) passed = sum(1 for t in result.test_results if t.passed) total = len(result.test_results) self._passed_tests += passed self._total_tests += total if result.error: status_str = "Error" row_color = Colors.ERROR else: status_str = str(result.status) row_color = Colors.SUCCESS if result.status < 400 else Colors.ERROR test_str = f"{passed}/{total}" if total > 0 else "-" item = QTreeWidgetItem([ f"{result.method} {result.request_name}", status_str, f"{result.elapsed_ms:.0f} ms", test_str, ]) item.setForeground(1, QBrush(QColor(row_color))) self.result_tree.addTopLevelItem(item) for tr in result.test_results: icon = "✓" if tr.passed else "✗" child = QTreeWidgetItem([f" {icon} {tr.name}", "", "", tr.message]) child.setForeground(0, QBrush(QColor(Colors.SUCCESS if tr.passed else Colors.ERROR))) item.addChild(child) item.setExpanded(True) def _on_finished(self): self.run_btn.setEnabled(True) self.summary_label.setText( f"Completed: {self._done} request(s) - " f"Tests: {self._passed_tests}/{self._total_tests} passed" )