---
title: "VIP Report"
format:
  html:
    code-fold: true
jupyter: python3
---

# Verified Installation of Posit

```{python}
#| echo: false

from pathlib import Path
from IPython.display import Markdown, display
from vip.reporting import load_results

data = load_results(Path("results.json"))

if data.total == 0:
    display(Markdown("> **No results found.** Run `pytest` to generate results."))
```

**Deployment:** `{python} data.deployment_name`
**Generated:** `{python} data.generated_at_display`

## Products Under Test

```{python}
#| echo: false

configured = data.configured_products()
if not configured:
    display(Markdown("No products configured. Set product URLs in `vip.toml`."))
else:
    rows = []
    for p in configured:
        name = p.name.replace("_", " ").title()
        ver = p.version or "—"
        rows.append(f"| {name} | {p.url} | {ver} |")
    header = "| Product | URL | Version |\n|---|---|---|"
    display(Markdown(header + "\n" + "\n".join(rows)))
```

## Summary

```{python}
#| echo: false

status = "PASS" if data.failed == 0 and data.total > 0 else "FAIL" if data.failed > 0 else "NO RESULTS"
color = "green" if status == "PASS" else "red" if status == "FAIL" else "gray"

display(Markdown(f"""
| Metric | Count |
|--------|-------|
| **Total** | {data.total} |
| **Passed** | {data.passed} |
| **Failed** | {data.failed} |
| **Skipped** | {data.skipped} |
| **Status** | **<span style="color:{color}">{status}</span>** |
"""))
```

## Results by Category

```{python}
#| echo: false

from html import escape as _esc
from IPython.display import HTML

from vip.gherkin import parse_feature_file

_feature_cache: dict[str, dict] = {}


def _get_feature(nodeid: str) -> dict | None:
    py_file = nodeid.split("::")[0] if "::" in nodeid else nodeid
    feature_file = py_file.rsplit(".", 1)[0] + ".feature"
    if feature_file not in _feature_cache:
        p = Path("..") / feature_file
        if p.exists():
            _feature_cache[feature_file] = parse_feature_file(p)
        else:
            _feature_cache[feature_file] = {}
    return _feature_cache[feature_file] or None


def _get_steps(item) -> list[str]:
    feature = _get_feature(item.nodeid)
    if not feature or not feature.get("scenarios") or not item.scenario_title:
        return []
    for sc in feature["scenarios"]:
        if sc["title"] == item.scenario_title and sc.get("steps"):
            return sc["steps"]
    return []


_OUTCOME_STYLES = {
    "passed": ("PASS", "#16a34a", "#f0fdf4", "#dcfce7"),
    "failed": ("FAIL", "#dc2626", "#fef2f2", "#fecaca"),
    "skipped": ("SKIP", "#6b7280", "#f9fafb", "#e5e7eb"),
}

_BORDER_COLORS = {
    "passed": "#16a34a",
    "failed": "#dc2626",
    "skipped": "#d1d5db",
}

categories = data.by_category()
if not categories:
    display(Markdown("No test results to display."))
else:
    _html_parts = []
    _html_parts.append("""
<style>
.vip-cat-section { margin-bottom: 2rem; }
.vip-cat-header {
  font-size: 1.25rem;
  font-weight: 700;
  margin: 0 0 0.25rem;
  color: #1f2937;
}
.vip-cat-counts {
  font-size: 0.8125rem;
  color: #6b7280;
  margin-bottom: 0.75rem;
}
.vip-test-card {
  border: 1px solid #e5e7eb;
  border-left: 4px solid #d1d5db;
  border-radius: 6px;
  padding: 0.75rem 1rem;
  margin-bottom: 0.5rem;
  background: #fefefe;
}
.vip-test-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.vip-test-scenario {
  font-weight: 600;
  font-size: 0.9375rem;
  color: #1f2937;
}
.vip-badge {
  display: inline-block;
  font-size: 0.6875rem;
  font-weight: 700;
  padding: 0.1rem 0.4rem;
  border-radius: 3px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.vip-test-nodeid {
  font-size: 0.75rem;
  color: #9ca3af;
  font-family: monospace;
  margin-top: 0.125rem;
}
.vip-test-steps {
  margin-top: 0.375rem;
}
.vip-test-steps summary {
  cursor: pointer;
  font-size: 0.8125rem;
  font-weight: 600;
  color: #6b7280;
  padding: 0.25rem 0;
}
.vip-test-steps summary:hover { color: #374151; }
.vip-step-list {
  margin: 0.25rem 0 0 1.25rem;
  padding: 0;
  list-style: none;
}
.vip-step-list li {
  font-size: 0.8125rem;
  font-family: monospace;
  color: #374151;
  line-height: 1.6;
}
.vip-step-list li::before {
  content: "› ";
  color: #9ca3af;
}
</style>
""")

    for cat, items in sorted(categories.items()):
        cat_label = cat.replace("_", " ").title()
        cat_passed = sum(1 for i in items if i.outcome == "passed")
        cat_failed = sum(1 for i in items if i.outcome == "failed")
        cat_skipped = sum(1 for i in items if i.outcome == "skipped")

        counts = []
        if cat_passed:
            counts.append(f"{cat_passed} passed")
        if cat_failed:
            counts.append(f"{cat_failed} failed")
        if cat_skipped:
            counts.append(f"{cat_skipped} skipped")

        _html_parts.append(
            f'<div class="vip-cat-section">'
            f'<h3 class="vip-cat-header">{_esc(cat_label)}</h3>'
            f'<p class="vip-cat-counts">'
            f"{len(items)} tests — {', '.join(counts)}</p>"
        )

        for item in items:
            label, fg, bg, border_bg = _OUTCOME_STYLES.get(
                item.outcome, ("?", "#6b7280", "#f9fafb", "#e5e7eb")
            )
            border_color = _BORDER_COLORS.get(item.outcome, "#d1d5db")
            title = _esc(item.scenario_title) if item.scenario_title else _esc(
                item.nodeid.split("::")[-1] if "::" in item.nodeid else item.nodeid
            )
            nodeid = _esc(item.nodeid)
            badge = (
                f'<span class="vip-badge" '
                f'style="color:{fg};background:{border_bg}">{label}</span>'
            )

            steps = _get_steps(item)
            steps_html = ""
            if steps:
                step_items = "".join(
                    f"<li>{_esc(s)}</li>" for s in steps
                )
                steps_html = (
                    f'<details class="vip-test-steps">'
                    f"<summary>Test procedure</summary>"
                    f'<ul class="vip-step-list">{step_items}</ul>'
                    f"</details>"
                )

            _html_parts.append(
                f'<div class="vip-test-card" style="border-left-color:{border_color}">'
                f'<div class="vip-test-header">'
                f"{badge}"
                f'<span class="vip-test-scenario">{title}</span>'
                f"</div>"
                f'<div class="vip-test-nodeid">{nodeid}</div>'
                f"{steps_html}"
                f"</div>"
            )

        _html_parts.append("</div>")

    display(HTML("\n".join(_html_parts)))
```

## Failed Tests

```{python}
#| echo: false

from html import escape as _esc
from IPython.display import HTML

from vip.reporting import load_troubleshooting

hints = load_troubleshooting(Path("../tests/troubleshooting.toml"))

failures = [r for r in data.results if r.outcome == "failed"]
if not failures:
    display(Markdown("No failures."))
else:
    _cards = []
    for _idx, f in enumerate(failures):
        hint = hints.get(f.scenario_title, {}) if f.scenario_title else {}

        _title = _esc(f.nodeid)
        _scenario = ""
        if f.scenario_title:
            desc = f" — {_esc(f.feature_description)}" if f.feature_description else ""
            _scenario = (
                f'<p class="vip-fail-scenario">'
                f"<strong>What was tested:</strong> {_esc(f.scenario_title)}{desc}</p>"
            )

        _error = ""
        if f.longrepr:
            _error_id = f"vip-error-{_idx}"
            _error = (
                f'<details class="vip-fail-details">'
                f"<summary>Error traceback</summary>"
                f'<div class="vip-fail-error-wrap">'
                f'<button class="vip-copy-btn" data-target="{_error_id}"'
                f' title="Copy to clipboard">Copy</button>'
                f'<pre id="{_error_id}" class="vip-fail-error">'
                f"{_esc(f.longrepr)}</pre>"
                f"</div></details>"
            )

        _troubleshoot = ""
        if hint:
            _parts = []
            if hint.get("likely_causes"):
                _parts.append("<strong>Likely causes:</strong><ul>")
                for cause in hint["likely_causes"]:
                    _parts.append(f"<li>{_esc(cause)}</li>")
                _parts.append("</ul>")
            if hint.get("suggested_steps"):
                _parts.append("<strong>Suggested next steps:</strong><ol>")
                for step in hint["suggested_steps"]:
                    _parts.append(f"<li>{_esc(step)}</li>")
                _parts.append("</ol>")
            if hint.get("docs_url"):
                _url = _esc(hint["docs_url"])
                _parts.append(
                    f'<p><strong>Documentation:</strong> '
                    f'<a href="{_url}" target="_blank" rel="noopener">{_url}</a></p>'
                )
            if _parts:
                _troubleshoot = (
                    f'<div class="vip-fail-hints">{"".join(_parts)}</div>'
                )

        _cards.append(
            f'<div class="vip-fail-card">'
            f'<h3 class="vip-fail-title"><code>{_title}</code></h3>'
            f"{_scenario}{_error}{_troubleshoot}"
            f"</div>"
        )

    _style = """
<style>
.vip-fail-card {
  border: 1px solid #e5e7eb;
  border-left: 4px solid #dc2626;
  border-radius: 6px;
  padding: 1.25rem 1.5rem;
  margin-bottom: 1.25rem;
  background: #fefefe;
}
.vip-fail-title {
  font-size: 1rem;
  margin: 0 0 0.5rem;
}
.vip-fail-title code {
  font-size: 0.875rem;
  color: #dc2626;
  background: #fef2f2;
  padding: 0.15em 0.4em;
  border-radius: 3px;
}
.vip-fail-scenario {
  margin: 0.25rem 0 0.75rem;
  font-size: 0.9375rem;
  color: #374151;
}
.vip-fail-details {
  margin-bottom: 0.75rem;
}
.vip-fail-details summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 0.875rem;
  color: #4b5563;
  padding: 0.375rem 0;
}
.vip-fail-details summary:hover {
  color: #1f2937;
}
.vip-fail-error-wrap {
  position: relative;
  margin-top: 0.375rem;
}
.vip-fail-error {
  background: #1e1e1e;
  color: #d4d4d4;
  font-size: 0.8125rem;
  line-height: 1.5;
  padding: 1rem;
  border-radius: 4px;
  overflow-x: auto;
  margin: 0;
  white-space: pre-wrap;
  word-break: break-word;
}
.vip-copy-btn {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  background: rgba(255,255,255,0.1);
  color: rgba(255,255,255,0.7);
  border: 1px solid rgba(255,255,255,0.2);
  border-radius: 4px;
  padding: 0.2rem 0.5rem;
  font-size: 0.75rem;
  font-family: inherit;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
  z-index: 1;
}
.vip-copy-btn:hover {
  background: rgba(255,255,255,0.2);
  color: #fff;
}
.vip-fail-hints {
  background: #fffbeb;
  border: 1px solid #fbbf24;
  border-radius: 4px;
  padding: 0.875rem 1rem;
  font-size: 0.875rem;
  color: #92400e;
}
.vip-fail-hints ul, .vip-fail-hints ol {
  margin: 0.25rem 0 0.75rem 1.25rem;
  padding: 0;
}
.vip-fail-hints li {
  margin-bottom: 0.25rem;
}
.vip-fail-hints p {
  margin: 0.25rem 0;
}
.vip-fail-hints a {
  color: #92400e;
  text-decoration: underline;
}
</style>
"""

    _script = """
<script>
document.querySelectorAll('.vip-copy-btn').forEach(function(btn) {
  btn.addEventListener('click', function() {
    var target = document.getElementById(btn.getAttribute('data-target'));
    if (!target) return;
    navigator.clipboard.writeText(target.textContent).then(function() {
      btn.textContent = 'Copied!';
      setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
    });
  });
});
</script>
"""

    display(HTML(_style + "\n".join(_cards) + _script))
```

```{python}
#| echo: false

if failures:
    display(Markdown(f"_`failures.json` is written by `pytest` alongside `results.json`._"))
```
