rails-profiler 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -323,6 +323,11 @@
323
323
  var u4 = p2(t2++, 7);
324
324
  return C2(u4.__H, r3) && (u4.__ = n2(), u4.__H = r3, u4.__h = n2), u4.__;
325
325
  }
326
+ function q2(n2, t3) {
327
+ return o2 = 8, T2(function() {
328
+ return n2;
329
+ }, t3);
330
+ }
326
331
  function j2() {
327
332
  for (var n2; n2 = f2.shift(); ) {
328
333
  var t3 = n2.__H;
@@ -1274,10 +1279,10 @@
1274
1279
  const overrideCount = Object.keys(overrides).length;
1275
1280
  const allEntries = Object.entries(variables);
1276
1281
  const filteredEntries = T2(() => {
1277
- const q2 = search.toLowerCase().trim();
1278
- if (!q2) return allEntries;
1282
+ const q3 = search.toLowerCase().trim();
1283
+ if (!q3) return allEntries;
1279
1284
  return allEntries.filter(
1280
- ([k3, v3]) => k3.toLowerCase().includes(q2) || v3.toLowerCase().includes(q2)
1285
+ ([k3, v3]) => k3.toLowerCase().includes(q3) || v3.toLowerCase().includes(q3)
1281
1286
  );
1282
1287
  }, [variables, search]);
1283
1288
  const groups = T2(() => {
@@ -1605,8 +1610,363 @@
1605
1610
  ] });
1606
1611
  }
1607
1612
 
1608
- // app/assets/typescript/profiler/components/ProfileList.tsx
1613
+ // app/assets/typescript/profiler/components/test-runner/TestFileTree.tsx
1614
+ function TestFileTree({ tree, selected, onToggleFile, onToggleDir }) {
1615
+ const [collapsed, setCollapsed] = d2(/* @__PURE__ */ new Set());
1616
+ const toggleCollapse = (dir) => {
1617
+ setCollapsed((prev) => {
1618
+ const next = new Set(prev);
1619
+ next.has(dir) ? next.delete(dir) : next.add(dir);
1620
+ return next;
1621
+ });
1622
+ };
1623
+ const dirSelected = (paths) => {
1624
+ const count = paths.filter((p3) => selected.has(p3)).length;
1625
+ if (count === 0) return "none";
1626
+ if (count === paths.length) return "all";
1627
+ return "partial";
1628
+ };
1629
+ return /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--mono", style: "overflow-y: auto; height: 100%", children: tree.map(({ directory, files }) => {
1630
+ const paths = files.map((f4) => f4.path);
1631
+ const sel = dirSelected(paths);
1632
+ const isCollapsed = collapsed.has(directory);
1633
+ return /* @__PURE__ */ u3("div", { style: "margin-bottom: 4px", children: [
1634
+ /* @__PURE__ */ u3(
1635
+ "div",
1636
+ {
1637
+ style: "display: flex; align-items: center; gap: 6px; padding: 3px 6px; cursor: pointer; border-radius: 4px; background: var(--profiler-bg-secondary, rgba(255,255,255,0.05))",
1638
+ onClick: () => toggleCollapse(directory),
1639
+ children: [
1640
+ /* @__PURE__ */ u3(
1641
+ "input",
1642
+ {
1643
+ type: "checkbox",
1644
+ checked: sel === "all",
1645
+ ref: (el) => {
1646
+ if (el) el.indeterminate = sel === "partial";
1647
+ },
1648
+ onClick: (e3) => {
1649
+ e3.stopPropagation();
1650
+ onToggleDir(directory, paths);
1651
+ },
1652
+ style: "cursor: pointer; flex-shrink: 0"
1653
+ }
1654
+ ),
1655
+ /* @__PURE__ */ u3("span", { style: "color: var(--profiler-text-muted, #888); flex-shrink: 0", children: isCollapsed ? "\u25B6" : "\u25BC" }),
1656
+ /* @__PURE__ */ u3("span", { style: "color: var(--profiler-accent, #06b6d4); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap", title: directory, children: [
1657
+ directory,
1658
+ "/"
1659
+ ] }),
1660
+ /* @__PURE__ */ u3("span", { style: "color: var(--profiler-text-muted, #888); margin-left: auto; flex-shrink: 0", children: [
1661
+ "(",
1662
+ files.length,
1663
+ ")"
1664
+ ] })
1665
+ ]
1666
+ }
1667
+ ),
1668
+ !isCollapsed && files.map((file) => /* @__PURE__ */ u3(
1669
+ "div",
1670
+ {
1671
+ style: "display: flex; align-items: center; gap: 6px; padding: 2px 6px 2px 24px; cursor: pointer; border-radius: 4px",
1672
+ onClick: () => onToggleFile(file.path),
1673
+ children: [
1674
+ /* @__PURE__ */ u3(
1675
+ "input",
1676
+ {
1677
+ type: "checkbox",
1678
+ checked: selected.has(file.path),
1679
+ onClick: (e3) => {
1680
+ e3.stopPropagation();
1681
+ onToggleFile(file.path);
1682
+ },
1683
+ style: "cursor: pointer; flex-shrink: 0"
1684
+ }
1685
+ ),
1686
+ /* @__PURE__ */ u3("span", { style: "overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--profiler-text, #e2e8f0)", title: file.path, children: file.name })
1687
+ ]
1688
+ },
1689
+ file.path
1690
+ ))
1691
+ ] }, directory);
1692
+ }) });
1693
+ }
1694
+
1695
+ // app/assets/typescript/profiler/components/test-runner/RunOutput.tsx
1696
+ function statusColor(status) {
1697
+ if (status === "passed") return "var(--profiler-success, #10b981)";
1698
+ if (status === "failed") return "var(--profiler-error, #ef4444)";
1699
+ if (status === "running") return "var(--profiler-accent, #06b6d4)";
1700
+ if (status === "killed" || status === "error") return "var(--profiler-warning, #f59e0b)";
1701
+ return "var(--profiler-text-muted, #888)";
1702
+ }
1703
+ function statusLabel(status) {
1704
+ const map = {
1705
+ pending: "\u23F3 Pending",
1706
+ running: "\u25CF Running\u2026",
1707
+ passed: "\u2713 Passed",
1708
+ failed: "\u2717 Failed",
1709
+ killed: "\u25A0 Killed",
1710
+ error: "\u26A0 Error"
1711
+ };
1712
+ return map[status] || status;
1713
+ }
1714
+ function parseAnsi(raw) {
1715
+ const ANSI_STYLES = {
1716
+ "0": "",
1717
+ "1": "font-weight:bold",
1718
+ "31": "color:var(--ansi-red)",
1719
+ "32": "color:var(--ansi-green)",
1720
+ "33": "color:var(--ansi-yellow)",
1721
+ "34": "color:var(--ansi-blue)",
1722
+ "35": "color:var(--ansi-purple)",
1723
+ "36": "color:var(--ansi-cyan)"
1724
+ };
1725
+ const spans = [];
1726
+ let currentStyle = "";
1727
+ const parts = raw.split(/(\x1b\[[0-9;]*m)/);
1728
+ for (const part of parts) {
1729
+ if (part.startsWith("\x1B[") && part.endsWith("m")) {
1730
+ const codes = part.slice(2, -1).split(";");
1731
+ if (codes.includes("0")) {
1732
+ currentStyle = "";
1733
+ } else {
1734
+ const styles = codes.map((c3) => ANSI_STYLES[c3] ?? "").filter(Boolean);
1735
+ currentStyle = [...currentStyle ? [currentStyle] : [], ...styles].join(";");
1736
+ }
1737
+ } else if (part.length > 0) {
1738
+ spans.push({ text: part, style: currentStyle });
1739
+ }
1740
+ }
1741
+ return spans;
1742
+ }
1743
+ function AnsiOutput({ text }) {
1744
+ const spans = parseAnsi(text);
1745
+ if (spans.length === 0) {
1746
+ return /* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted)", children: "No output yet\u2026" });
1747
+ }
1748
+ return /* @__PURE__ */ u3(k, { children: spans.map(
1749
+ (s3, i3) => s3.style ? /* @__PURE__ */ u3("span", { style: s3.style, children: s3.text }, i3) : /* @__PURE__ */ u3("span", { children: s3.text }, i3)
1750
+ ) });
1751
+ }
1752
+ function RunOutput({ run }) {
1753
+ const outputRef = A2(null);
1754
+ y2(() => {
1755
+ if (outputRef.current && run?.status === "running") {
1756
+ outputRef.current.scrollTop = outputRef.current.scrollHeight;
1757
+ }
1758
+ }, [run?.output]);
1759
+ if (!run) {
1760
+ return /* @__PURE__ */ u3("div", { class: "profiler-empty", style: "height: 100%; display: flex; align-items: center; justify-content: center", children: /* @__PURE__ */ u3("div", { children: [
1761
+ /* @__PURE__ */ u3("div", { class: "profiler-empty__title", style: "font-size: 1.1rem", children: "Select files and click Run" }),
1762
+ /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: "Test output will appear here in real time" })
1763
+ ] }) });
1764
+ }
1765
+ return /* @__PURE__ */ u3("div", { style: "display: flex; flex-direction: column; height: 100%; gap: 12px", children: [
1766
+ /* @__PURE__ */ u3("div", { style: "display: flex; align-items: center; gap: 12px; flex-shrink: 0", children: [
1767
+ /* @__PURE__ */ u3("span", { style: `font-weight: 600; color: ${statusColor(run.status)}`, children: statusLabel(run.status) }),
1768
+ run.duration != null && /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--muted", children: [
1769
+ (run.duration / 1e3).toFixed(1),
1770
+ "s"
1771
+ ] }),
1772
+ (run.status === "passed" || run.status === "failed") && /* @__PURE__ */ u3("a", { href: "/_profiler?section=tests", class: "profiler-text--xs", style: "color: var(--profiler-accent, #06b6d4); margin-left: auto", children: "View in Profiler \u2192" })
1773
+ ] }),
1774
+ /* @__PURE__ */ u3(
1775
+ "pre",
1776
+ {
1777
+ ref: outputRef,
1778
+ style: "flex: 1; overflow-y: auto; background: var(--profiler-terminal-bg); color: var(--profiler-terminal-text); padding: 14px 16px; border-radius: 6px; font-family: var(--profiler-font-mono); font-size: 12px; line-height: 1.65; margin: 0; white-space: pre-wrap; word-break: break-all; border: 1px solid var(--profiler-border)",
1779
+ children: /* @__PURE__ */ u3(AnsiOutput, { text: run.output || "" })
1780
+ }
1781
+ ),
1782
+ run.files.length > 0 && /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "flex-shrink: 0", children: [
1783
+ run.files.length,
1784
+ " file",
1785
+ run.files.length !== 1 ? "s" : "",
1786
+ " \xB7 ",
1787
+ run.framework
1788
+ ] })
1789
+ ] });
1790
+ }
1791
+
1792
+ // app/assets/typescript/profiler/components/test-runner/TestRunnerContent.tsx
1609
1793
  var BASE = "/_profiler";
1794
+ function TestRunnerContent() {
1795
+ const [framework, setFramework] = d2("");
1796
+ const [frameworks, setFrameworks] = d2([]);
1797
+ const [tree, setTree] = d2([]);
1798
+ const [selected, setSelected] = d2(/* @__PURE__ */ new Set());
1799
+ const [loading, setLoading] = d2(true);
1800
+ const [currentRun, setCurrentRun] = d2(null);
1801
+ const [isRunning, setIsRunning] = d2(false);
1802
+ const [error, setError] = d2(null);
1803
+ const loadFiles = q2((fw) => {
1804
+ setLoading(true);
1805
+ fetch(`${BASE}/api/test_runner/files?framework=${fw}`).then((r3) => r3.json()).then((data) => {
1806
+ setFrameworks(data.frameworks || []);
1807
+ setTree(data.tree || []);
1808
+ setSelected(/* @__PURE__ */ new Set());
1809
+ setLoading(false);
1810
+ }).catch(() => {
1811
+ setError("Failed to load test files");
1812
+ setLoading(false);
1813
+ });
1814
+ }, []);
1815
+ y2(() => {
1816
+ fetch(`${BASE}/api/test_runner/files`).then((r3) => r3.json()).then((data) => {
1817
+ const fws = (data.frameworks || []).map(String);
1818
+ setFrameworks(fws);
1819
+ const defaultFw = fws[0] || "minitest";
1820
+ setFramework(defaultFw);
1821
+ return fetch(`${BASE}/api/test_runner/files?framework=${defaultFw}`);
1822
+ }).then((r3) => r3.json()).then((data) => {
1823
+ setTree(data.tree || []);
1824
+ setLoading(false);
1825
+ }).catch(() => {
1826
+ setError("Failed to load test files");
1827
+ setLoading(false);
1828
+ });
1829
+ }, []);
1830
+ y2(() => {
1831
+ if (!currentRun || !isRunning) return;
1832
+ if (["passed", "failed", "killed", "error"].includes(currentRun.status)) {
1833
+ setIsRunning(false);
1834
+ return;
1835
+ }
1836
+ const es = new EventSource(`${BASE}/api/test_runner/runs/${currentRun.id}/stream`);
1837
+ es.addEventListener("output", (e3) => {
1838
+ try {
1839
+ const data = JSON.parse(e3.data);
1840
+ setCurrentRun((prev) => prev ? { ...prev, output: (prev.output || "") + data.chunk } : prev);
1841
+ } catch {
1842
+ }
1843
+ });
1844
+ es.addEventListener("done", (e3) => {
1845
+ try {
1846
+ const data = JSON.parse(e3.data);
1847
+ setCurrentRun((prev) => prev ? { ...prev, status: data.status } : prev);
1848
+ } catch {
1849
+ }
1850
+ setIsRunning(false);
1851
+ es.close();
1852
+ });
1853
+ es.onerror = () => {
1854
+ es.close();
1855
+ fetch(`${BASE}/api/test_runner/runs/${currentRun.id}`).then((r3) => r3.json()).then((data) => {
1856
+ setCurrentRun(data);
1857
+ setIsRunning(false);
1858
+ }).catch(() => setIsRunning(false));
1859
+ };
1860
+ return () => es.close();
1861
+ }, [currentRun?.id, isRunning]);
1862
+ const handleFrameworkChange = (fw) => {
1863
+ setFramework(fw);
1864
+ loadFiles(fw);
1865
+ };
1866
+ const toggleFile = (path) => {
1867
+ setSelected((prev) => {
1868
+ const next = new Set(prev);
1869
+ next.has(path) ? next.delete(path) : next.add(path);
1870
+ return next;
1871
+ });
1872
+ };
1873
+ const toggleDir = (_dir, paths) => {
1874
+ setSelected((prev) => {
1875
+ const next = new Set(prev);
1876
+ const allSelected = paths.every((p3) => next.has(p3));
1877
+ if (allSelected) {
1878
+ paths.forEach((p3) => next.delete(p3));
1879
+ } else {
1880
+ paths.forEach((p3) => next.add(p3));
1881
+ }
1882
+ return next;
1883
+ });
1884
+ };
1885
+ const selectAll = () => {
1886
+ const all = tree.flatMap((d3) => d3.files.map((f4) => f4.path));
1887
+ setSelected(new Set(all));
1888
+ };
1889
+ const selectNone = () => setSelected(/* @__PURE__ */ new Set());
1890
+ const runTests = () => {
1891
+ if (selected.size === 0 || isRunning) return;
1892
+ setError(null);
1893
+ fetch(`${BASE}/api/test_runner/runs`, {
1894
+ method: "POST",
1895
+ headers: { "Content-Type": "application/json" },
1896
+ body: JSON.stringify({ files: Array.from(selected), framework })
1897
+ }).then((r3) => r3.json()).then((data) => {
1898
+ setCurrentRun(data);
1899
+ setIsRunning(true);
1900
+ }).catch(() => setError("Failed to start test run"));
1901
+ };
1902
+ const stopRun = () => {
1903
+ if (!currentRun) return;
1904
+ fetch(`${BASE}/api/test_runner/runs/${currentRun.id}`, { method: "DELETE" }).then(() => {
1905
+ setIsRunning(false);
1906
+ setCurrentRun((prev) => prev ? { ...prev, status: "killed" } : prev);
1907
+ }).catch(() => {
1908
+ });
1909
+ };
1910
+ const totalFiles = tree.reduce((n2, d3) => n2 + d3.files.length, 0);
1911
+ return /* @__PURE__ */ u3("div", { children: [
1912
+ error && /* @__PURE__ */ u3("div", { style: "color: var(--profiler-error, #ef4444); background: rgba(239,68,68,0.1); border: 1px solid var(--profiler-error, #ef4444); border-radius: 4px; padding: 8px 12px; margin-bottom: 12px; font-size: 13px", children: error }),
1913
+ /* @__PURE__ */ u3("div", { class: "profiler-action-bar profiler-mb-3", children: [
1914
+ /* @__PURE__ */ u3("div", { class: "profiler-filter-group", children: frameworks.length > 0 && frameworks.map((fw) => /* @__PURE__ */ u3(
1915
+ "button",
1916
+ {
1917
+ class: `profiler-preset-btn${framework === fw ? " profiler-preset-btn--active" : ""}`,
1918
+ onClick: () => handleFrameworkChange(fw),
1919
+ disabled: isRunning,
1920
+ children: fw === "rspec" ? "RSpec" : "Minitest"
1921
+ },
1922
+ fw
1923
+ )) }),
1924
+ /* @__PURE__ */ u3("div", { class: "profiler-filter-group", style: "margin-left: auto", children: [
1925
+ /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--muted", children: [
1926
+ selected.size,
1927
+ " / ",
1928
+ totalFiles,
1929
+ " selected"
1930
+ ] }),
1931
+ /* @__PURE__ */ u3("button", { class: "btn btn-secondary btn-sm", onClick: selectAll, disabled: isRunning || loading, children: "All" }),
1932
+ /* @__PURE__ */ u3("button", { class: "btn btn-secondary btn-sm", onClick: selectNone, disabled: isRunning, children: "None" }),
1933
+ isRunning ? /* @__PURE__ */ u3("button", { class: "btn btn-danger btn-sm", onClick: stopRun, children: "\u25A0 Stop" }) : /* @__PURE__ */ u3(
1934
+ "button",
1935
+ {
1936
+ class: "btn btn-sm",
1937
+ style: "background: var(--profiler-accent, #06b6d4); color: #000; font-weight: 600",
1938
+ onClick: runTests,
1939
+ disabled: selected.size === 0,
1940
+ children: [
1941
+ "\u25B6 Run Selected (",
1942
+ selected.size,
1943
+ ")"
1944
+ ]
1945
+ }
1946
+ )
1947
+ ] })
1948
+ ] }),
1949
+ /* @__PURE__ */ u3("div", { style: "display: grid; grid-template-columns: 260px 1fr; height: 460px; border: 1px solid var(--profiler-border, rgba(255,255,255,0.1)); border-radius: 6px; overflow: hidden", children: [
1950
+ /* @__PURE__ */ u3("div", { style: "border-right: 1px solid var(--profiler-border, rgba(255,255,255,0.1)); overflow-y: auto; padding: 8px", children: loading ? /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "padding: 8px", children: "Loading files\u2026" }) : tree.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "padding: 8px", children: [
1951
+ "No ",
1952
+ framework,
1953
+ " test files found"
1954
+ ] }) : /* @__PURE__ */ u3(
1955
+ TestFileTree,
1956
+ {
1957
+ tree,
1958
+ selected,
1959
+ onToggleFile: toggleFile,
1960
+ onToggleDir: toggleDir
1961
+ }
1962
+ ) }),
1963
+ /* @__PURE__ */ u3("div", { style: "overflow: hidden; display: flex; flex-direction: column", children: /* @__PURE__ */ u3(RunOutput, { run: currentRun }) })
1964
+ ] })
1965
+ ] });
1966
+ }
1967
+
1968
+ // app/assets/typescript/profiler/components/ProfileList.tsx
1969
+ var BASE2 = "/_profiler";
1610
1970
  function TableSkeleton({ cols, rows = 6 }) {
1611
1971
  return /* @__PURE__ */ u3("div", { children: Array.from({ length: rows }).map((_2, i3) => /* @__PURE__ */ u3("div", { class: "profiler-skeleton__row", children: cols.map((size, j3) => /* @__PURE__ */ u3("div", { class: `profiler-skeleton__cell profiler-skeleton__cell--${size}` }, j3)) }, i3)) });
1612
1972
  }
@@ -1644,7 +2004,7 @@
1644
2004
  const params = new URLSearchParams(window.location.search);
1645
2005
  const initialSection = () => {
1646
2006
  const s3 = params.get("section");
1647
- return s3 === "http" || s3 === "jobs" || s3 === "console" || s3 === "outbound" || s3 === "env" ? s3 : "http";
2007
+ return s3 === "http" || s3 === "jobs" || s3 === "console" || s3 === "tests" || s3 === "runner" || s3 === "outbound" || s3 === "env" ? s3 : "http";
1648
2008
  };
1649
2009
  const initialSort = () => {
1650
2010
  const col = params.get("sort");
@@ -1667,6 +2027,13 @@
1667
2027
  const [consoleOffset, setConsoleOffset] = d2(0);
1668
2028
  const [consoleHasMore, setConsoleHasMore] = d2(false);
1669
2029
  const [consoleLoadingMore, setConsoleLoadingMore] = d2(false);
2030
+ const [tests, setTests] = d2([]);
2031
+ const [testOffset, setTestOffset] = d2(0);
2032
+ const [testHasMore, setTestHasMore] = d2(false);
2033
+ const [testLoadingMore, setTestLoadingMore] = d2(false);
2034
+ const [loadingTests, setLoadingTests] = d2(initialSection() === "tests");
2035
+ const [testsLoaded, setTestsLoaded] = d2(false);
2036
+ const [testsError, setTestsError] = d2(null);
1670
2037
  const [outboundRequests, setOutboundRequests] = d2([]);
1671
2038
  const [loadingHttp, setLoadingHttp] = d2(initialSection() === "http");
1672
2039
  const [loadingJobs, setLoadingJobs] = d2(initialSection() === "jobs");
@@ -1696,6 +2063,9 @@
1696
2063
  const [jobDuration, setJobDuration] = d2("");
1697
2064
  const [consoleSearch, setConsoleSearch] = d2("");
1698
2065
  const [consoleStatus, setConsoleStatus] = d2("");
2066
+ const [testSearch, setTestSearch] = d2("");
2067
+ const [testStatus, setTestStatus] = d2("");
2068
+ const [testSort, setTestSort] = d2({ col: null, dir: "asc" });
1699
2069
  const [outboundSearch, setOutboundSearch] = d2("");
1700
2070
  const [outboundMethod, setOutboundMethod] = d2("");
1701
2071
  const [outboundStatus, setOutboundStatus] = d2("");
@@ -1733,7 +2103,7 @@
1733
2103
  };
1734
2104
  const loadMoreHttp = () => {
1735
2105
  setHttpLoadingMore(true);
1736
- fetch(`${BASE}/api/profiles?limit=50&offset=${httpOffset}`).then((res) => res.json()).then((data) => {
2106
+ fetch(`${BASE2}/api/profiles?limit=50&offset=${httpOffset}`).then((res) => res.json()).then((data) => {
1737
2107
  setProfiles((prev) => [...prev, ...data.profiles]);
1738
2108
  setHttpOffset((prev) => prev + data.profiles.length);
1739
2109
  setHttpHasMore(data.has_more);
@@ -1742,7 +2112,7 @@
1742
2112
  };
1743
2113
  const loadMoreJobs = () => {
1744
2114
  setJobLoadingMore(true);
1745
- fetch(`${BASE}/api/jobs?limit=50&offset=${jobOffset}`).then((res) => res.json()).then((data) => {
2115
+ fetch(`${BASE2}/api/jobs?limit=50&offset=${jobOffset}`).then((res) => res.json()).then((data) => {
1746
2116
  setJobs((prev) => [...prev, ...data.profiles]);
1747
2117
  setJobOffset((prev) => prev + data.profiles.length);
1748
2118
  setJobHasMore(data.has_more);
@@ -1751,13 +2121,37 @@
1751
2121
  };
1752
2122
  const loadMoreConsole = () => {
1753
2123
  setConsoleLoadingMore(true);
1754
- fetch(`${BASE}/api/console?limit=50&offset=${consoleOffset}`).then((res) => res.json()).then((data) => {
2124
+ fetch(`${BASE2}/api/console?limit=50&offset=${consoleOffset}`).then((res) => res.json()).then((data) => {
1755
2125
  setConsoles((prev) => [...prev, ...data.profiles]);
1756
2126
  setConsoleOffset((prev) => prev + data.profiles.length);
1757
2127
  setConsoleHasMore(data.has_more);
1758
2128
  setConsoleLoadingMore(false);
1759
2129
  }).catch(() => setConsoleLoadingMore(false));
1760
2130
  };
2131
+ const loadTests = () => {
2132
+ if (testsLoaded) return;
2133
+ setLoadingTests(true);
2134
+ fetch(`${BASE2}/api/tests?limit=50&offset=0`).then((res) => res.json()).then((data) => {
2135
+ setTests(data.profiles);
2136
+ setTestOffset(data.profiles.length);
2137
+ setTestHasMore(data.has_more);
2138
+ setLoadingTests(false);
2139
+ setTestsLoaded(true);
2140
+ }).catch(() => {
2141
+ setTestsError("Failed to load test profiles");
2142
+ setLoadingTests(false);
2143
+ setTestsLoaded(true);
2144
+ });
2145
+ };
2146
+ const loadMoreTests = () => {
2147
+ setTestLoadingMore(true);
2148
+ fetch(`${BASE2}/api/tests?limit=50&offset=${testOffset}`).then((res) => res.json()).then((data) => {
2149
+ setTests((prev) => [...prev, ...data.profiles]);
2150
+ setTestOffset((prev) => prev + data.profiles.length);
2151
+ setTestHasMore(data.has_more);
2152
+ setTestLoadingMore(false);
2153
+ }).catch(() => setTestLoadingMore(false));
2154
+ };
1761
2155
  const handleSectionChange = (s3) => {
1762
2156
  if (s3 === section) {
1763
2157
  refreshSection(s3);
@@ -1767,7 +2161,11 @@
1767
2161
  const url = new URL(window.location.href);
1768
2162
  url.searchParams.set("section", s3);
1769
2163
  history.pushState(null, "", url.toString());
1770
- refreshSection(s3);
2164
+ if (s3 === "tests") {
2165
+ loadTests();
2166
+ } else {
2167
+ refreshSection(s3);
2168
+ }
1771
2169
  setHttpSearch("");
1772
2170
  setHttpMethod("");
1773
2171
  setHttpStatus("");
@@ -1781,6 +2179,9 @@
1781
2179
  setConsoleSearch("");
1782
2180
  setConsoleStatus("");
1783
2181
  setConsoleSort({ col: null, dir: "asc" });
2182
+ setTestSearch("");
2183
+ setTestStatus("");
2184
+ setTestSort({ col: null, dir: "asc" });
1784
2185
  setOutboundSearch("");
1785
2186
  setOutboundMethod("");
1786
2187
  setOutboundStatus("");
@@ -1792,44 +2193,55 @@
1792
2193
  });
1793
2194
  };
1794
2195
  const deleteProfile = (token) => {
1795
- fetch(`${BASE}/api/profiles/${token}`, { method: "DELETE" }).then(() => {
2196
+ fetch(`${BASE2}/api/profiles/${token}`, { method: "DELETE" }).then(() => {
1796
2197
  setProfiles((prev) => prev.filter((p3) => p3.token !== token));
1797
2198
  });
1798
2199
  };
1799
2200
  const deleteJob = (token) => {
1800
- fetch(`${BASE}/api/jobs/${token}`, { method: "DELETE" }).then(() => {
2201
+ fetch(`${BASE2}/api/jobs/${token}`, { method: "DELETE" }).then(() => {
1801
2202
  setJobs((prev) => prev.filter((p3) => p3.token !== token));
1802
2203
  });
1803
2204
  };
1804
2205
  const deleteConsole = (token) => {
1805
- fetch(`${BASE}/api/console/${token}`, { method: "DELETE" }).then(() => {
2206
+ fetch(`${BASE2}/api/console/${token}`, { method: "DELETE" }).then(() => {
1806
2207
  setConsoles((prev) => prev.filter((p3) => p3.token !== token));
1807
2208
  });
1808
2209
  };
2210
+ const deleteTest = (token) => {
2211
+ fetch(`${BASE2}/api/tests/${token}`, { method: "DELETE" }).then(() => {
2212
+ setTests((prev) => prev.filter((p3) => p3.token !== token));
2213
+ });
2214
+ };
1809
2215
  const clearProfiles = () => {
1810
2216
  if (!window.confirm("Delete all HTTP profiles?")) return;
1811
- fetch(`${BASE}/api/profiles/clear`, { method: "DELETE" }).then(() => {
2217
+ fetch(`${BASE2}/api/profiles/clear`, { method: "DELETE" }).then(() => {
1812
2218
  setProfiles([]);
1813
2219
  });
1814
2220
  };
1815
2221
  const clearJobs = () => {
1816
2222
  if (!window.confirm("Delete all job profiles?")) return;
1817
- fetch(`${BASE}/api/jobs/clear`, { method: "DELETE" }).then(() => {
2223
+ fetch(`${BASE2}/api/jobs/clear`, { method: "DELETE" }).then(() => {
1818
2224
  setJobs([]);
1819
2225
  });
1820
2226
  };
1821
2227
  const clearConsole = () => {
1822
2228
  if (!window.confirm("Delete all console profiles?")) return;
1823
- fetch(`${BASE}/api/console/clear`, { method: "DELETE" }).then(() => {
2229
+ fetch(`${BASE2}/api/console/clear`, { method: "DELETE" }).then(() => {
1824
2230
  setConsoles([]);
1825
2231
  });
1826
2232
  };
2233
+ const clearTests = () => {
2234
+ if (!window.confirm("Delete all test profiles?")) return;
2235
+ fetch(`${BASE2}/api/tests/clear`, { method: "DELETE" }).then(() => {
2236
+ setTests([]);
2237
+ });
2238
+ };
1827
2239
  const clearAll = () => {
1828
2240
  if (!window.confirm("Delete all HTTP, job and console profiles?")) return;
1829
2241
  Promise.all([
1830
- fetch(`${BASE}/api/profiles/clear`, { method: "DELETE" }),
1831
- fetch(`${BASE}/api/jobs/clear`, { method: "DELETE" }),
1832
- fetch(`${BASE}/api/console/clear`, { method: "DELETE" })
2242
+ fetch(`${BASE2}/api/profiles/clear`, { method: "DELETE" }),
2243
+ fetch(`${BASE2}/api/jobs/clear`, { method: "DELETE" }),
2244
+ fetch(`${BASE2}/api/console/clear`, { method: "DELETE" })
1833
2245
  ]).then(() => {
1834
2246
  setProfiles([]);
1835
2247
  setJobs([]);
@@ -1839,7 +2251,7 @@
1839
2251
  const refreshSection = (s3) => {
1840
2252
  if (s3 === "http") {
1841
2253
  setLoadingHttp(true);
1842
- fetch(`${BASE}/api/profiles?limit=50&offset=0`).then((res) => res.json()).then((data) => {
2254
+ fetch(`${BASE2}/api/profiles?limit=50&offset=0`).then((res) => res.json()).then((data) => {
1843
2255
  setProfiles(data.profiles);
1844
2256
  setHttpOffset(data.profiles.length);
1845
2257
  setHttpHasMore(data.has_more);
@@ -1850,7 +2262,7 @@
1850
2262
  });
1851
2263
  } else if (s3 === "jobs") {
1852
2264
  setLoadingJobs(true);
1853
- fetch(`${BASE}/api/jobs?limit=50&offset=0`).then((res) => res.json()).then((data) => {
2265
+ fetch(`${BASE2}/api/jobs?limit=50&offset=0`).then((res) => res.json()).then((data) => {
1854
2266
  setJobs(data.profiles);
1855
2267
  setJobOffset(data.profiles.length);
1856
2268
  setJobHasMore(data.has_more);
@@ -1861,7 +2273,7 @@
1861
2273
  });
1862
2274
  } else if (s3 === "console") {
1863
2275
  setLoadingConsole(true);
1864
- fetch(`${BASE}/api/console?limit=50&offset=0`).then((res) => res.json()).then((data) => {
2276
+ fetch(`${BASE2}/api/console?limit=50&offset=0`).then((res) => res.json()).then((data) => {
1865
2277
  setConsoles(data.profiles);
1866
2278
  setConsoleOffset(data.profiles.length);
1867
2279
  setConsoleHasMore(data.has_more);
@@ -1870,18 +2282,32 @@
1870
2282
  setConsoleError("Failed to load console profiles");
1871
2283
  setLoadingConsole(false);
1872
2284
  });
2285
+ } else if (s3 === "tests") {
2286
+ setLoadingTests(true);
2287
+ setTestsLoaded(false);
2288
+ fetch(`${BASE2}/api/tests?limit=50&offset=0`).then((res) => res.json()).then((data) => {
2289
+ setTests(data.profiles);
2290
+ setTestOffset(data.profiles.length);
2291
+ setTestHasMore(data.has_more);
2292
+ setLoadingTests(false);
2293
+ setTestsLoaded(true);
2294
+ }).catch(() => {
2295
+ setTestsError("Failed to load test profiles");
2296
+ setLoadingTests(false);
2297
+ setTestsLoaded(true);
2298
+ });
1873
2299
  } else if (s3 === "outbound") {
1874
2300
  setLoadingOutbound(true);
1875
- fetch(`${BASE}/api/outbound_http`).then((res) => res.json()).then((data) => {
2301
+ fetch(`${BASE2}/api/outbound_http`).then((res) => res.json()).then((data) => {
1876
2302
  setOutboundRequests(data);
1877
2303
  setLoadingOutbound(false);
1878
2304
  }).catch(() => {
1879
2305
  setOutboundError("Failed to load outbound HTTP requests");
1880
2306
  setLoadingOutbound(false);
1881
2307
  });
1882
- } else {
2308
+ } else if (s3 === "env") {
1883
2309
  setLoadingEnv(true);
1884
- fetch(`${BASE}/api/env_vars`).then((res) => res.json()).then((data) => {
2310
+ fetch(`${BASE2}/api/env_vars`).then((res) => res.json()).then((data) => {
1885
2311
  setEnvData(data);
1886
2312
  setLoadingEnv(false);
1887
2313
  }).catch(() => {
@@ -2009,9 +2435,44 @@
2009
2435
  }
2010
2436
  return consoleSort.dir === "asc" ? av - bv : bv - av;
2011
2437
  }) : filteredConsoles;
2438
+ const filteredTests = tests.filter((p3) => {
2439
+ if (testSearch) {
2440
+ const testData = p3.collectors_data?.test;
2441
+ const name = (testData?.test_name || p3.path).toLowerCase();
2442
+ if (!name.includes(testSearch.toLowerCase())) return false;
2443
+ }
2444
+ if (testStatus === "failed" && p3.status !== 500) return false;
2445
+ if (testStatus === "passed" && p3.status === 500) return false;
2446
+ if (testStatus === "pending") {
2447
+ const testData = p3.collectors_data?.test;
2448
+ if (testData?.status !== "pending") return false;
2449
+ }
2450
+ return true;
2451
+ });
2452
+ const sortedTests = testSort.col ? [...filteredTests].sort((a3, b) => {
2453
+ let av, bv;
2454
+ if (testSort.col === "date") {
2455
+ const diff = new Date(a3.started_at).getTime() - new Date(b.started_at).getTime();
2456
+ return testSort.dir === "asc" ? diff : -diff;
2457
+ }
2458
+ switch (testSort.col) {
2459
+ case "duration":
2460
+ av = a3.duration;
2461
+ bv = b.duration;
2462
+ break;
2463
+ case "status":
2464
+ av = a3.status;
2465
+ bv = b.status;
2466
+ break;
2467
+ default:
2468
+ return 0;
2469
+ }
2470
+ return testSort.dir === "asc" ? av - bv : bv - av;
2471
+ }) : filteredTests;
2012
2472
  const httpFiltersActive = !!(httpSearch || httpMethod || httpStatus || httpDuration || httpPreset);
2013
2473
  const jobFiltersActive = !!(jobSearch || jobStatus || jobDuration);
2014
2474
  const consoleFiltersActive = !!(consoleSearch || consoleStatus);
2475
+ const testFiltersActive = !!(testSearch || testStatus);
2015
2476
  const outboundFiltersActive = !!(outboundSearch || outboundMethod || outboundStatus);
2016
2477
  const sortIcon = (activeCol, dir, col) => {
2017
2478
  if (activeCol !== col) return /* @__PURE__ */ u3("span", { class: "sort-icon sort-icon--idle", children: "\u21C5" });
@@ -2043,7 +2504,15 @@
2043
2504
  e3.preventDefault();
2044
2505
  handleSectionChange("console");
2045
2506
  }, children: "Console" }),
2507
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("tests"), onClick: (e3) => {
2508
+ e3.preventDefault();
2509
+ handleSectionChange("tests");
2510
+ }, children: "Tests" }),
2046
2511
  /* @__PURE__ */ u3("div", { style: "flex: 1" }),
2512
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("runner"), onClick: (e3) => {
2513
+ e3.preventDefault();
2514
+ handleSectionChange("runner");
2515
+ }, children: "Test Runner" }),
2047
2516
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("outbound"), onClick: (e3) => {
2048
2517
  e3.preventDefault();
2049
2518
  handleSectionChange("outbound");
@@ -2143,7 +2612,7 @@
2143
2612
  /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
2144
2613
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: methodClass(p3.method), children: p3.method }) }),
2145
2614
  /* @__PURE__ */ u3("td", { children: [
2146
- /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${p3.token}`, children: p3.path }),
2615
+ /* @__PURE__ */ u3("a", { href: `${BASE2}/profiles/${p3.token}`, children: p3.path }),
2147
2616
  p3.gem_version && p3.gem_version !== currentVersion && /* @__PURE__ */ u3("span", { class: "profiler-version-warn", title: `Captur\xE9 avec v${p3.gem_version} (actuel : v${currentVersion})`, children: "\u26A0\uFE0F" })
2148
2617
  ] }),
2149
2618
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
@@ -2224,7 +2693,7 @@
2224
2693
  return /* @__PURE__ */ u3("tr", { children: [
2225
2694
  /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
2226
2695
  /* @__PURE__ */ u3("td", { children: [
2227
- /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${p3.token}`, children: p3.path }),
2696
+ /* @__PURE__ */ u3("a", { href: `${BASE2}/profiles/${p3.token}`, children: p3.path }),
2228
2697
  p3.gem_version && p3.gem_version !== currentVersion && /* @__PURE__ */ u3("span", { class: "profiler-version-warn", title: `Captur\xE9 avec v${p3.gem_version} (actuel : v${currentVersion})`, children: "\u26A0\uFE0F" })
2229
2698
  ] }),
2230
2699
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", children: jobData?.queue || "-" }) }),
@@ -2302,7 +2771,7 @@
2302
2771
  return /* @__PURE__ */ u3("tr", { children: [
2303
2772
  /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
2304
2773
  /* @__PURE__ */ u3("td", { children: [
2305
- /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${p3.token}`, class: "profiler-text--mono", style: "font-size:0.85em;", children: p3.path.length > 60 ? p3.path.slice(0, 60) + "\u2026" : p3.path }),
2774
+ /* @__PURE__ */ u3("a", { href: `${BASE2}/profiles/${p3.token}`, class: "profiler-text--mono", style: "font-size:0.85em;", children: p3.path.length > 60 ? p3.path.slice(0, 60) + "\u2026" : p3.path }),
2306
2775
  p3.gem_version && p3.gem_version !== currentVersion && /* @__PURE__ */ u3("span", { class: "profiler-version-warn", title: `Captur\xE9 avec v${p3.gem_version} (actuel : v${currentVersion})`, children: "\u26A0\uFE0F" })
2307
2776
  ] }),
2308
2777
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
@@ -2318,6 +2787,90 @@
2318
2787
  ] }),
2319
2788
  consoleHasMore && !consoleFiltersActive && /* @__PURE__ */ u3("div", { class: "profiler-load-more", children: /* @__PURE__ */ u3("button", { class: "btn btn-secondary", onClick: loadMoreConsole, disabled: consoleLoadingMore, children: consoleLoadingMore ? "Loading\u2026" : "Load more" }) })
2320
2789
  ] })),
2790
+ section === "tests" && (loadingTests ? /* @__PURE__ */ u3(TableSkeleton, { cols: ["sm", "flex", "md", "sm", "xs", "sm"] }) : testsError ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: testsError }) }) : tests.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
2791
+ /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No test profiles found" }),
2792
+ /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: [
2793
+ "Add ",
2794
+ /* @__PURE__ */ u3("code", { children: "Profiler::TestHelpers::RSpecSupport.install(config)" }),
2795
+ " to your spec_helper.rb and run your tests. Or use the ",
2796
+ /* @__PURE__ */ u3("a", { href: "#", style: "color:var(--profiler-accent,#06b6d4)", onClick: (e3) => {
2797
+ e3.preventDefault();
2798
+ handleSectionChange("runner");
2799
+ }, children: "Test Runner" }),
2800
+ " tab to run tests from here."
2801
+ ] })
2802
+ ] }) : /* @__PURE__ */ u3(k, { children: [
2803
+ /* @__PURE__ */ u3("div", { class: "profiler-action-bar profiler-mb-3", children: [
2804
+ /* @__PURE__ */ u3("div", { class: "profiler-filter-group", children: [
2805
+ /* @__PURE__ */ u3(
2806
+ "input",
2807
+ {
2808
+ type: "text",
2809
+ class: "profiler-filter-input",
2810
+ placeholder: "Search test name\u2026",
2811
+ value: testSearch,
2812
+ onInput: (e3) => setTestSearch(e3.target.value)
2813
+ }
2814
+ ),
2815
+ /* @__PURE__ */ u3("select", { class: "profiler-filter-select", value: testStatus, onChange: (e3) => setTestStatus(e3.target.value), children: [
2816
+ /* @__PURE__ */ u3("option", { value: "", children: "All Statuses" }),
2817
+ /* @__PURE__ */ u3("option", { value: "passed", children: "Passed" }),
2818
+ /* @__PURE__ */ u3("option", { value: "failed", children: "Failed" }),
2819
+ /* @__PURE__ */ u3("option", { value: "pending", children: "Pending" })
2820
+ ] })
2821
+ ] }),
2822
+ /* @__PURE__ */ u3("div", { class: "profiler-filter-group", children: [
2823
+ testFiltersActive && /* @__PURE__ */ u3("span", { class: "profiler-filter-count", children: [
2824
+ filteredTests.length,
2825
+ " / ",
2826
+ tests.length
2827
+ ] }),
2828
+ /* @__PURE__ */ u3("button", { class: `btn-refresh${loadingTests ? " btn-refresh--spinning" : ""}`, onClick: refresh, disabled: loadingTests, title: "Refresh", children: "\u21BA" }),
2829
+ /* @__PURE__ */ u3("button", { class: "btn btn-danger btn-sm", onClick: clearTests, title: "Delete test profiles", children: "Clear All" })
2830
+ ] })
2831
+ ] }),
2832
+ filteredTests.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No results match filters" }) }) : /* @__PURE__ */ u3("table", { children: [
2833
+ /* @__PURE__ */ u3("thead", { children: /* @__PURE__ */ u3("tr", { children: [
2834
+ /* @__PURE__ */ u3("th", { class: `sortable${testSort.col === "date" ? " sortable--active" : ""}`, onClick: () => setTestSort((prev) => ({ col: "date", dir: prev.col === "date" && prev.dir === "asc" ? "desc" : "asc" })), children: [
2835
+ "Time ",
2836
+ sortIcon(testSort.col, testSort.dir, "date")
2837
+ ] }),
2838
+ /* @__PURE__ */ u3("th", { children: "Test Name" }),
2839
+ /* @__PURE__ */ u3("th", { children: "File" }),
2840
+ /* @__PURE__ */ u3("th", { class: `sortable${testSort.col === "duration" ? " sortable--active" : ""}`, onClick: () => setTestSort((prev) => ({ col: "duration", dir: prev.col === "duration" && prev.dir === "asc" ? "desc" : "asc" })), children: [
2841
+ "Duration ",
2842
+ sortIcon(testSort.col, testSort.dir, "duration")
2843
+ ] }),
2844
+ /* @__PURE__ */ u3("th", { children: "Queries" }),
2845
+ /* @__PURE__ */ u3("th", { class: `sortable${testSort.col === "status" ? " sortable--active" : ""}`, onClick: () => setTestSort((prev) => ({ col: "status", dir: prev.col === "status" && prev.dir === "asc" ? "desc" : "asc" })), children: [
2846
+ "Status ",
2847
+ sortIcon(testSort.col, testSort.dir, "status")
2848
+ ] }),
2849
+ /* @__PURE__ */ u3("th", { children: "Token" }),
2850
+ /* @__PURE__ */ u3("th", {})
2851
+ ] }) }),
2852
+ /* @__PURE__ */ u3("tbody", { children: sortedTests.map((p3) => {
2853
+ const testData = p3.collectors_data?.test;
2854
+ const isFailed = p3.status === 500;
2855
+ const status = testData?.status || (isFailed ? "failed" : "passed");
2856
+ return /* @__PURE__ */ u3("tr", { children: [
2857
+ /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
2858
+ /* @__PURE__ */ u3("td", { style: "max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap", children: /* @__PURE__ */ u3("a", { href: `${BASE2}/profiles/${p3.token}`, title: testData?.test_name || p3.path, children: testData?.test_name || p3.path }) }),
2859
+ /* @__PURE__ */ u3("td", { class: "profiler-text--xs profiler-text--mono profiler-text--muted", children: testData?.test_file || "-" }),
2860
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
2861
+ p3.duration.toFixed(2),
2862
+ " ms"
2863
+ ] }) }),
2864
+ /* @__PURE__ */ u3("td", { children: p3.collectors_data?.database?.total_queries ?? "\u2014" }),
2865
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: status === "failed" ? "badge-error" : status === "pending" ? "badge-warning" : "badge-success", children: status === "failed" ? "\u2717 Failed" : status === "pending" ? "\u23F8 Pending" : "\u2713 Passed" }) }),
2866
+ /* @__PURE__ */ u3("td", { class: "profiler-text--xs profiler-text--mono profiler-text--muted", children: /* @__PURE__ */ u3("button", { class: "token-copy", onClick: () => copyToken(p3.token), title: "Copy full token", children: copiedToken === p3.token ? "\u2713" : p3.token.substring(0, 8) + "\u2026" }) }),
2867
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("button", { class: "btn-row-delete", onClick: () => deleteTest(p3.token), title: "Delete", children: "\xD7" }) })
2868
+ ] }, p3.token);
2869
+ }) })
2870
+ ] }),
2871
+ testHasMore && !testFiltersActive && /* @__PURE__ */ u3("div", { class: "profiler-load-more", children: /* @__PURE__ */ u3("button", { class: "btn btn-secondary", onClick: loadMoreTests, disabled: testLoadingMore, children: testLoadingMore ? "Loading\u2026" : "Load more" }) })
2872
+ ] })),
2873
+ section === "runner" && /* @__PURE__ */ u3(TestRunnerContent, {}),
2321
2874
  section === "outbound" && (loadingOutbound ? /* @__PURE__ */ u3(TableSkeleton, { cols: ["sm", "xs", "flex", "sm", "xs"], rows: 4 }) : outboundError ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: outboundError }) }) : outboundRequests.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
2322
2875
  /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No outbound HTTP requests found" }),
2323
2876
  /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: "Make requests to external services to see outbound HTTP data" })
@@ -2370,7 +2923,7 @@
2370
2923
  /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "margin-bottom:2px", children: [
2371
2924
  formatTime(req.profile_started_at),
2372
2925
  " \xB7 Profile: ",
2373
- /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${req.profile_token}`, children: [
2926
+ /* @__PURE__ */ u3("a", { href: `${BASE2}/profiles/${req.profile_token}`, children: [
2374
2927
  req.profile_token.substring(0, 8),
2375
2928
  "\u2026"
2376
2929
  ] })
@@ -4386,8 +4939,8 @@
4386
4939
  const verbs = ["ALL", ...Array.from(new Set(routes.map((r3) => r3.verb))).sort()];
4387
4940
  const filtered = routes.filter((route) => {
4388
4941
  const matchesVerb = verbFilter === "ALL" || route.verb === verbFilter;
4389
- const q2 = filter.toLowerCase();
4390
- const matchesText = !q2 || (route.pattern ?? "").toLowerCase().includes(q2) || (route.name ?? "").toLowerCase().includes(q2) || (route.controller_action ?? "").toLowerCase().includes(q2);
4942
+ const q3 = filter.toLowerCase();
4943
+ const matchesText = !q3 || (route.pattern ?? "").toLowerCase().includes(q3) || (route.name ?? "").toLowerCase().includes(q3) || (route.controller_action ?? "").toLowerCase().includes(q3);
4391
4944
  return matchesVerb && matchesText;
4392
4945
  });
4393
4946
  return /* @__PURE__ */ u3(k, { children: [
@@ -5186,6 +5739,155 @@
5186
5739
  ] });
5187
5740
  }
5188
5741
 
5742
+ // app/assets/typescript/profiler/components/dashboard/tabs/TestTab.tsx
5743
+ function statusBadge2(status) {
5744
+ if (status === "passed") return /* @__PURE__ */ u3("span", { class: "badge-success", children: "\u2713 Passed" });
5745
+ if (status === "failed") return /* @__PURE__ */ u3("span", { class: "badge-error", children: "\u2717 Failed" });
5746
+ if (status === "pending") return /* @__PURE__ */ u3("span", { class: "badge-warning", children: "\u23F8 Pending" });
5747
+ return /* @__PURE__ */ u3("span", { class: "badge-default", children: status });
5748
+ }
5749
+ function TestTab({ testData }) {
5750
+ return /* @__PURE__ */ u3("div", { children: /* @__PURE__ */ u3("table", { class: "profiler-detail-table", children: /* @__PURE__ */ u3("tbody", { children: [
5751
+ /* @__PURE__ */ u3("tr", { children: [
5752
+ /* @__PURE__ */ u3("th", { children: "Test Name" }),
5753
+ /* @__PURE__ */ u3("td", { style: "word-break: break-word", children: testData.test_name })
5754
+ ] }),
5755
+ /* @__PURE__ */ u3("tr", { children: [
5756
+ /* @__PURE__ */ u3("th", { children: "Status" }),
5757
+ /* @__PURE__ */ u3("td", { children: statusBadge2(testData.status) })
5758
+ ] }),
5759
+ /* @__PURE__ */ u3("tr", { children: [
5760
+ /* @__PURE__ */ u3("th", { children: "File" }),
5761
+ /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: [
5762
+ testData.test_file,
5763
+ ":",
5764
+ testData.test_line
5765
+ ] })
5766
+ ] }),
5767
+ /* @__PURE__ */ u3("tr", { children: [
5768
+ /* @__PURE__ */ u3("th", { children: "Framework" }),
5769
+ /* @__PURE__ */ u3("td", { class: "profiler-text--mono profiler-text--xs", children: testData.framework })
5770
+ ] }),
5771
+ testData.assertions != null && /* @__PURE__ */ u3("tr", { children: [
5772
+ /* @__PURE__ */ u3("th", { children: "Assertions" }),
5773
+ /* @__PURE__ */ u3("td", { children: testData.assertions })
5774
+ ] }),
5775
+ testData.skip_reason && /* @__PURE__ */ u3("tr", { children: [
5776
+ /* @__PURE__ */ u3("th", { children: "Skip reason" }),
5777
+ /* @__PURE__ */ u3("td", { class: "profiler-text--xs profiler-text--muted", children: testData.skip_reason })
5778
+ ] }),
5779
+ testData.exception_message && /* @__PURE__ */ u3("tr", { children: [
5780
+ /* @__PURE__ */ u3("th", { children: "Exception" }),
5781
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("pre", { class: "profiler-text--xs", style: "white-space: pre-wrap; color: var(--profiler-error, #ef4444)", children: testData.exception_message }) })
5782
+ ] })
5783
+ ] }) }) });
5784
+ }
5785
+
5786
+ // app/assets/typescript/profiler/components/dashboard/TestProfileDashboard.tsx
5787
+ function TestProfileDashboard({ profile, initialTab, embedded }) {
5788
+ const cd = profile.collectors_data || {};
5789
+ const hasDumps = (cd["dump"]?.count ?? 0) > 0;
5790
+ const hasLogs = (cd["logs"]?.total ?? 0) > 0;
5791
+ const hasException = !!cd["exception"]?.exception_class;
5792
+ const testData = cd["test"];
5793
+ const validTabs = ["test", "database", "cache", "dump", "logs", "exception", "env", "timeline"];
5794
+ const defaultTab = validTabs.includes(initialTab) ? initialTab : "test";
5795
+ const [activeTab, setActiveTab] = d2(hasException ? "exception" : defaultTab);
5796
+ const isFailed = profile.status === 500;
5797
+ const testStatus = testData?.status || (isFailed ? "failed" : "passed");
5798
+ const handleTabClick = (tab) => (e3) => {
5799
+ e3.preventDefault();
5800
+ setActiveTab(tab);
5801
+ const url = new URL(window.location.href);
5802
+ url.searchParams.set("tab", tab);
5803
+ history.pushState(null, "", url.toString());
5804
+ };
5805
+ const tabClass = (key) => `tab${activeTab === key ? " active" : ""}`;
5806
+ return /* @__PURE__ */ u3("div", { class: "container", children: [
5807
+ /* @__PURE__ */ u3("div", { class: "header", children: [
5808
+ /* @__PURE__ */ u3("h1", { children: /* @__PURE__ */ u3("a", { href: "/_profiler?section=tests", children: [
5809
+ /* @__PURE__ */ u3("span", { class: "h1-emoji", children: "\u{1F9EA}" }),
5810
+ " Test Profile"
5811
+ ] }) }),
5812
+ /* @__PURE__ */ u3("p", { style: "word-break: break-word", children: testData?.test_name || profile.path }),
5813
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-4 profiler-mt-2", children: [
5814
+ /* @__PURE__ */ u3("span", { children: [
5815
+ "Duration: ",
5816
+ /* @__PURE__ */ u3("strong", { children: [
5817
+ profile.duration.toFixed(2),
5818
+ " ms"
5819
+ ] })
5820
+ ] }),
5821
+ /* @__PURE__ */ u3("span", { children: [
5822
+ "Status: ",
5823
+ /* @__PURE__ */ u3("strong", { children: /* @__PURE__ */ u3("span", { class: `badge-${isFailed ? "error" : testStatus === "pending" ? "warning" : "success"}`, children: testStatus === "failed" ? "\u2717 Failed" : testStatus === "pending" ? "\u23F8 Pending" : "\u2713 Passed" }) })
5824
+ ] }),
5825
+ profile.memory != null && /* @__PURE__ */ u3("span", { children: [
5826
+ "Memory: ",
5827
+ /* @__PURE__ */ u3("strong", { children: [
5828
+ (profile.memory / 1024 / 1024).toFixed(2),
5829
+ " MB"
5830
+ ] })
5831
+ ] })
5832
+ ] }),
5833
+ testData?.test_file && /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted profiler-mt-2", children: [
5834
+ /* @__PURE__ */ u3("span", { class: "profiler-text--mono", children: [
5835
+ testData.test_file,
5836
+ ":",
5837
+ testData.test_line
5838
+ ] }),
5839
+ /* @__PURE__ */ u3("span", { style: "margin-left: 8px", children: [
5840
+ "\xB7 ",
5841
+ testData.framework
5842
+ ] })
5843
+ ] })
5844
+ ] }),
5845
+ /* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: [
5846
+ /* @__PURE__ */ u3("div", { class: "tabs", children: [
5847
+ hasException && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("exception"), onClick: handleTabClick("exception"), style: "color:var(--profiler-error,#ef4444);", children: "\u{1F4A5} Exception" }),
5848
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("test"), onClick: handleTabClick("test"), children: "Test" }),
5849
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("database"), onClick: handleTabClick("database"), children: "Database" }),
5850
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("cache"), onClick: handleTabClick("cache"), children: "Cache" }),
5851
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("timeline"), onClick: handleTabClick("timeline"), children: "Timeline" }),
5852
+ hasDumps && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("dump"), onClick: handleTabClick("dump"), children: [
5853
+ "Dumps (",
5854
+ cd["dump"].count,
5855
+ ")"
5856
+ ] }),
5857
+ hasLogs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("logs"), onClick: handleTabClick("logs"), children: "Logs" }),
5858
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("env"), onClick: handleTabClick("env"), children: "Env" })
5859
+ ] }),
5860
+ /* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
5861
+ activeTab === "test" && testData && /* @__PURE__ */ u3(TestTab, { testData }),
5862
+ activeTab === "database" && /* @__PURE__ */ u3(DatabaseTab, { dbData: cd["database"], token: profile.token }),
5863
+ activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { data: cd["cache"] }),
5864
+ activeTab === "timeline" && /* @__PURE__ */ u3(FlameGraphTab, { data: cd["flamegraph"] }),
5865
+ activeTab === "dump" && hasDumps && /* @__PURE__ */ u3(DumpsTab, { data: cd["dump"] }),
5866
+ activeTab === "logs" && hasLogs && /* @__PURE__ */ u3(LogsTab, { data: cd["logs"] }),
5867
+ activeTab === "exception" && hasException && /* @__PURE__ */ u3(ExceptionTab, { data: cd["exception"] }),
5868
+ activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"] })
5869
+ ] })
5870
+ ] })
5871
+ ] });
5872
+ }
5873
+
5874
+ // app/assets/typescript/profiler/components/test-runner/TestRunnerPage.tsx
5875
+ var BASE3 = "/_profiler";
5876
+ function TestRunnerPage() {
5877
+ return /* @__PURE__ */ u3("div", { class: "container", children: [
5878
+ /* @__PURE__ */ u3("div", { class: "header", children: [
5879
+ /* @__PURE__ */ u3("h1", { children: [
5880
+ /* @__PURE__ */ u3("a", { href: BASE3, children: /* @__PURE__ */ u3("span", { class: "h1-emoji", children: "\u{1F50D}" }) }),
5881
+ " ",
5882
+ /* @__PURE__ */ u3("span", { class: "h1-emoji", children: "\u{1F9EA}" }),
5883
+ " Test Runner"
5884
+ ] }),
5885
+ /* @__PURE__ */ u3("p", { children: "Run tests from the profiler and capture a performance profile for each test." })
5886
+ ] }),
5887
+ /* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: /* @__PURE__ */ u3("div", { class: "profiler-p-4", children: /* @__PURE__ */ u3(TestRunnerContent, {}) }) })
5888
+ ] });
5889
+ }
5890
+
5189
5891
  // app/assets/typescript/profiler/timeline.ts
5190
5892
  function initTimeline(container) {
5191
5893
  console.log("Initializing profiler timeline");
@@ -5449,12 +6151,18 @@ Duration: ${event.duration.toFixed(2)}ms`;
5449
6151
  J(/* @__PURE__ */ u3(JobProfileDashboard, { profile, initialTab: tab, embedded }), showEl);
5450
6152
  } else if (profile.profile_type === "console") {
5451
6153
  J(/* @__PURE__ */ u3(ConsoleProfileDashboard, { profile, initialTab: tab, embedded }), showEl);
6154
+ } else if (profile.profile_type === "test") {
6155
+ J(/* @__PURE__ */ u3(TestProfileDashboard, { profile, initialTab: tab, embedded }), showEl);
5452
6156
  } else {
5453
6157
  J(/* @__PURE__ */ u3(ProfileDashboard, { profile, initialTab: tab, embedded }), showEl);
5454
6158
  }
5455
6159
  }
5456
6160
  }
5457
- if (!indexEl && !showEl) {
6161
+ const testRunnerEl = document.getElementById("profiler-test-runner");
6162
+ if (testRunnerEl) {
6163
+ J(/* @__PURE__ */ u3(TestRunnerPage, {}), testRunnerEl);
6164
+ }
6165
+ if (!indexEl && !showEl && !testRunnerEl) {
5458
6166
  const header = document.querySelector(".header, .profiler-detail");
5459
6167
  if (header) {
5460
6168
  const toggle = createThemeToggle();