From 5b3bbe72274912d20becadc7a3121d8da41a544d Mon Sep 17 00:00:00 2001 From: Alan Gali Date: Mon, 16 Mar 2026 15:32:59 +0800 Subject: [PATCH] CSS enhancements on the test results. --- src/ApiTestRunner.App/wwwroot/app.js | 140 +++++++++++++++++++---- src/ApiTestRunner.App/wwwroot/index.html | 3 + src/ApiTestRunner.App/wwwroot/styles.css | 33 ++++++ 3 files changed, 156 insertions(+), 20 deletions(-) diff --git a/src/ApiTestRunner.App/wwwroot/app.js b/src/ApiTestRunner.App/wwwroot/app.js index b7cedb6..d188b03 100644 --- a/src/ApiTestRunner.App/wwwroot/app.js +++ b/src/ApiTestRunner.App/wwwroot/app.js @@ -6,6 +6,9 @@ const expandSelectionButton = document.getElementById("expandSelectionButton"); const collapseSelectionButton = document.getElementById("collapseSelectionButton"); const expandResultsButton = document.getElementById("expandResultsButton"); const collapseResultsButton = document.getElementById("collapseResultsButton"); +const showAllResultsButton = document.getElementById("showAllResultsButton"); +const showPassingResultsButton = document.getElementById("showPassingResultsButton"); +const showFailingResultsButton = document.getElementById("showFailingResultsButton"); const selectionSearchInput = document.getElementById("selectionSearchInput"); const resultsSearchInput = document.getElementById("resultsSearchInput"); const selectionContainer = document.getElementById("selectionContainer"); @@ -21,6 +24,8 @@ let selectedTestIds = new Set(); let lastRunState = null; let selectionSearchTerm = ""; let resultsSearchTerm = ""; +let resultsStatusFilter = "all"; +let resultExpansionOverride = null; const selectionExpansionState = { environments: new Map(), @@ -337,7 +342,9 @@ function renderState(state) { lastRunState = state; const run = state.lastRun; const searchDisplayTerm = resultsSearchInput.value.trim(); + const autoExpandResults = Boolean(resultsSearchTerm) || resultsStatusFilter !== "all"; setStatusError(Boolean(state.lastError)); + updateResultFilterButtons(); document.getElementById("runStatus").textContent = buildStatusText(state); document.getElementById("startedAt").textContent = `Started: ${formatDate(state.lastStartedAtUtc)}`; @@ -361,21 +368,32 @@ function renderState(state) { return; } - const filteredEnvironments = filterRunEnvironments(run, resultsSearchTerm); + const filteredEnvironments = filterRunEnvironments(run, resultsSearchTerm, resultsStatusFilter); const visibleEndpointCount = filteredEnvironments.reduce((count, environmentEntry) => count + environmentEntry.endpoints.length, 0); const visibleTestCount = filteredEnvironments.reduce( (count, environmentEntry) => count + environmentEntry.endpoints.reduce((endpointCount, endpointEntry) => endpointCount + endpointEntry.tests.length, 0), 0 ); - - resultsSummary.textContent = resultsSearchTerm - ? `${run.passedTests} passed, ${run.failedTests} failed overall | ${visibleEndpointCount} endpoints and ${visibleTestCount} tests shown for "${searchDisplayTerm}"` - : `${run.passedTests} passed, ${run.failedTests} failed across ${run.environments.length} environments.`; + const visiblePassedTestCount = filteredEnvironments.reduce( + (count, environmentEntry) => count + environmentEntry.endpoints.reduce( + (endpointCount, endpointEntry) => endpointCount + endpointEntry.tests.filter((test) => test.isSuccess).length, + 0), + 0 + ); + const visibleFailedTestCount = visibleTestCount - visiblePassedTestCount; + + resultsSummary.textContent = buildResultsSummary( + run, + visibleEndpointCount, + visibleTestCount, + visiblePassedTestCount, + visibleFailedTestCount, + searchDisplayTerm); updateResultButtons(true); synchronizeResultExpansionState(run); if (filteredEnvironments.length === 0) { - environmentContainer.innerHTML = `

No matching results

No APIs, endpoints, tests, assertions, or errors match "${escapeHtml(searchDisplayTerm)}".

`; + environmentContainer.innerHTML = `

No matching results

${buildEmptyResultsMessage(searchDisplayTerm)}

`; return; } @@ -387,19 +405,26 @@ function renderState(state) { const visibleFailedTests = visibleTests.length - visiblePassedTests; const environmentNode = environmentTemplate.content.firstElementChild.cloneNode(true); - environmentNode.open = resultsSearchTerm ? true : resultExpansionState.environments.get(environmentKey) ?? environment.failedTests > 0; + environmentNode.open = resultExpansionOverride ?? (autoExpandResults ? true : resultExpansionState.environments.get(environmentKey) ?? environment.failedTests > 0); environmentNode.addEventListener("toggle", () => { resultExpansionState.environments.set(environmentKey, environmentNode.open); }); environmentNode.querySelector(".environment-name").innerHTML = highlightMatch(environment.name, resultsSearchTerm); environmentNode.querySelector(".environment-url").innerHTML = highlightMatch(environment.baseUrl, resultsSearchTerm); - environmentNode.querySelector(".environment-stats").textContent = resultsSearchTerm && !environmentMatches - ? `${visiblePassedTests} passed, ${visibleFailedTests} failed, ${visibleTests.length} matching tests` - : `${environment.passedTests} passed, ${environment.failedTests} failed, ${environment.totalTests} total`; + const environmentStats = environmentNode.querySelector(".environment-stats"); + const statsPassedCount = (resultsSearchTerm || resultsStatusFilter !== "all") ? visiblePassedTests : environment.passedTests; + const statsFailedCount = (resultsSearchTerm || resultsStatusFilter !== "all") ? visibleFailedTests : environment.failedTests; + const statsTotalCount = (resultsSearchTerm || resultsStatusFilter !== "all") ? visibleTests.length : environment.totalTests; + const statsTotalLabel = (resultsSearchTerm || resultsStatusFilter !== "all") ? "matching tests" : "total"; + environmentStats.innerHTML = [ + `${statsPassedCount} passed`, + `${statsFailedCount} failed`, + `${statsTotalCount} ${statsTotalLabel}` + ].join(", "); const environmentBadge = environmentNode.querySelector(".environment-badge"); - const environmentIsPassing = resultsSearchTerm ? visibleFailedTests === 0 : environment.failedTests === 0; + const environmentIsPassing = (resultsSearchTerm || resultsStatusFilter !== "all") ? visibleFailedTests === 0 : environment.failedTests === 0; environmentBadge.textContent = environmentIsPassing ? "Passing" : "Issues"; environmentBadge.className = `environment-badge ${environmentIsPassing ? "passing" : "failing"}`; @@ -409,7 +434,7 @@ function renderState(state) { const { endpoint, endpointMatches, tests } = endpointEntry; const endpointKey = getResultEndpointKey(environment, endpoint); const endpointNode = endpointTemplate.content.firstElementChild.cloneNode(true); - endpointNode.open = resultsSearchTerm ? true : resultExpansionState.endpoints.get(endpointKey) ?? !endpoint.isSuccess; + endpointNode.open = resultExpansionOverride ?? (autoExpandResults ? true : resultExpansionState.endpoints.get(endpointKey) ?? !endpoint.isSuccess); endpointNode.addEventListener("toggle", () => { resultExpansionState.endpoints.set(endpointKey, endpointNode.open); }); @@ -420,7 +445,7 @@ function renderState(state) { const endpointBadge = endpointNode.querySelector(".endpoint-badge"); const endpointVisibleFailedTests = tests.filter((test) => !test.isSuccess).length; - const endpointIsPassing = resultsSearchTerm ? endpointVisibleFailedTests === 0 : endpoint.isSuccess; + const endpointIsPassing = (resultsSearchTerm || resultsStatusFilter !== "all") ? endpointVisibleFailedTests === 0 : endpoint.isSuccess; endpointBadge.textContent = endpointIsPassing ? "Pass" : "Fail"; endpointBadge.className = `endpoint-badge ${endpointIsPassing ? "passing" : "failing"}`; @@ -434,7 +459,7 @@ function renderState(state) { tests.forEach((test, testIndex) => { const testKey = getResultTestKey(environment, endpoint, test, testIndex); const testNode = testTemplate.content.firstElementChild.cloneNode(true); - testNode.open = resultsSearchTerm ? true : resultExpansionState.tests.get(testKey) ?? !test.isSuccess; + testNode.open = resultExpansionOverride ?? (autoExpandResults ? true : resultExpansionState.tests.get(testKey) ?? !test.isSuccess); testNode.addEventListener("toggle", () => { resultExpansionState.tests.set(testKey, testNode.open); }); @@ -498,8 +523,8 @@ function initializeResponsePreview(endpointNode, responseText) { }); } -function filterRunEnvironments(run, searchTerm) { - if (!searchTerm) { +function filterRunEnvironments(run, searchTerm, statusFilter) { + if (!searchTerm && statusFilter === "all") { return run.environments.map((environment) => ({ environment, environmentMatches: false, @@ -520,17 +545,18 @@ function filterRunEnvironments(run, searchTerm) { const endpoints = environment.endpoints .map((endpoint) => { const endpointMatches = environmentMatches || endpointMatchesRunSearch(endpoint, searchTerm); + const visibleTests = endpoint.tests.filter((test) => shouldIncludeResultTest(test, statusFilter)); return { endpoint, endpointMatches, tests: endpointMatches - ? endpoint.tests - : endpoint.tests.filter((test) => testMatchesRunSearch(test, searchTerm)) + ? visibleTests + : visibleTests.filter((test) => testMatchesRunSearch(test, searchTerm)) }; }) - .filter((endpointEntry) => endpointEntry.endpointMatches || endpointEntry.tests.length > 0); + .filter((endpointEntry) => endpointEntry.tests.length > 0); - if (!environmentMatches && endpoints.length === 0) { + if (endpoints.length === 0) { return null; } @@ -543,6 +569,18 @@ function filterRunEnvironments(run, searchTerm) { .filter((entry) => entry !== null); } +function shouldIncludeResultTest(test, statusFilter) { + if (statusFilter === "passing") { + return test.isSuccess; + } + + if (statusFilter === "failing") { + return !test.isSuccess; + } + + return true; +} + function endpointMatchesRunSearch(endpoint, searchTerm) { return ( matchesSearch(endpoint.name, searchTerm) || @@ -693,6 +731,15 @@ function updateSelectionButtons(isEnabled) { function updateResultButtons(isEnabled) { expandResultsButton.disabled = !isEnabled; collapseResultsButton.disabled = !isEnabled; + showAllResultsButton.disabled = !isEnabled; + showPassingResultsButton.disabled = !isEnabled; + showFailingResultsButton.disabled = !isEnabled; +} + +function updateResultFilterButtons() { + showAllResultsButton.classList.toggle("is-active", resultsStatusFilter === "all"); + showPassingResultsButton.classList.toggle("is-active", resultsStatusFilter === "passing"); + showFailingResultsButton.classList.toggle("is-active", resultsStatusFilter === "failing"); } function setStatusError(hasError) { @@ -733,6 +780,8 @@ function setResultExpansion(isOpen) { return; } + resultExpansionOverride = isOpen; + for (const environment of run.environments) { resultExpansionState.environments.set(getResultEnvironmentKey(environment), isOpen); @@ -748,6 +797,53 @@ function setResultExpansion(isOpen) { renderState(lastRunState); } +function setResultsStatusFilter(filter) { + resultsStatusFilter = filter; + resultExpansionOverride = null; + updateResultFilterButtons(); + + if (lastRunState) { + renderState(lastRunState); + } +} + +function buildResultsSummary(run, visibleEndpointCount, visibleTestCount, visiblePassedTestCount, visibleFailedTestCount, searchDisplayTerm) { + if (!resultsSearchTerm && resultsStatusFilter === "all") { + return `${run.passedTests} passed, ${run.failedTests} failed across ${run.environments.length} environments.`; + } + + const filterDescription = resultsStatusFilter === "passing" + ? "passing tests" + : resultsStatusFilter === "failing" + ? "failing tests" + : "tests"; + const searchDescription = resultsSearchTerm ? ` for "${searchDisplayTerm}"` : ""; + + return `${run.passedTests} passed, ${run.failedTests} failed overall | showing ${visiblePassedTestCount} passed and ${visibleFailedTestCount} failed across ${visibleEndpointCount} endpoints and ${visibleTestCount} ${filterDescription}${searchDescription}`; +} + +function buildEmptyResultsMessage(searchDisplayTerm) { + const filterDescription = resultsStatusFilter === "passing" + ? "passing" + : resultsStatusFilter === "failing" + ? "failing" + : "visible"; + + if (resultsSearchTerm) { + return `No ${filterDescription} APIs, endpoints, tests, assertions, or errors match "${escapeHtml(searchDisplayTerm)}".`; + } + + if (resultsStatusFilter === "passing") { + return "No passing tests are visible in the latest results."; + } + + if (resultsStatusFilter === "failing") { + return "No failing tests are visible in the latest results."; + } + + return "No results are visible in the latest run."; +} + function matchesSearch(value, searchTerm) { return typeof value === "string" && value.toLowerCase().includes(searchTerm); } @@ -800,6 +896,9 @@ expandSelectionButton.addEventListener("click", () => setSelectionExpansion(true collapseSelectionButton.addEventListener("click", () => setSelectionExpansion(false)); expandResultsButton.addEventListener("click", () => setResultExpansion(true)); collapseResultsButton.addEventListener("click", () => setResultExpansion(false)); +showAllResultsButton.addEventListener("click", () => setResultsStatusFilter("all")); +showPassingResultsButton.addEventListener("click", () => setResultsStatusFilter("passing")); +showFailingResultsButton.addEventListener("click", () => setResultsStatusFilter("failing")); selectionSearchInput.addEventListener("input", () => { selectionSearchTerm = selectionSearchInput.value.trim().toLowerCase(); @@ -809,6 +908,7 @@ selectionSearchInput.addEventListener("input", () => { }); resultsSearchInput.addEventListener("input", () => { resultsSearchTerm = resultsSearchInput.value.trim().toLowerCase(); + resultExpansionOverride = null; if (lastRunState) { renderState(lastRunState); diff --git a/src/ApiTestRunner.App/wwwroot/index.html b/src/ApiTestRunner.App/wwwroot/index.html index 802a587..5c7b988 100644 --- a/src/ApiTestRunner.App/wwwroot/index.html +++ b/src/ApiTestRunner.App/wwwroot/index.html @@ -184,6 +184,9 @@

Use the controls to expand or collapse the latest results.

+ + +
diff --git a/src/ApiTestRunner.App/wwwroot/styles.css b/src/ApiTestRunner.App/wwwroot/styles.css index 44cc957..6834045 100644 --- a/src/ApiTestRunner.App/wwwroot/styles.css +++ b/src/ApiTestRunner.App/wwwroot/styles.css @@ -332,6 +332,25 @@ body.app-shell { transform: translateY(-1px); } +.ghost-button.is-active { + background: var(--results-panel-accent); + border-color: rgba(29, 78, 216, 0.22); + color: #fff; + box-shadow: 0 10px 22px rgba(29, 78, 216, 0.18); +} + +#showPassingResultsButton.is-active { + background: linear-gradient(135deg, #16a34a, #166534); + border-color: rgba(22, 163, 74, 0.24); + box-shadow: 0 10px 22px rgba(22, 163, 74, 0.18); +} + +#showFailingResultsButton.is-active { + background: linear-gradient(135deg, #dc2626, #991b1b); + border-color: rgba(220, 38, 38, 0.24); + box-shadow: 0 10px 22px rgba(220, 38, 38, 0.18); +} + .primary-button:disabled, .ghost-button:disabled { opacity: 0.65; @@ -616,6 +635,20 @@ body.app-shell { color: #667085; } +.environment-stats .stats-pass { + color: var(--app-pass); + font-weight: 700; +} + +.environment-stats .stats-fail { + color: var(--app-fail); + font-weight: 700; +} + +.environment-stats .stats-total { + color: inherit; +} + .selection-label-stack span { font-size: 0.85rem; }