diff --git a/src/ApiTestRunner.App/wwwroot/app.js b/src/ApiTestRunner.App/wwwroot/app.js index 5d800d8..b7cedb6 100644 --- a/src/ApiTestRunner.App/wwwroot/app.js +++ b/src/ApiTestRunner.App/wwwroot/app.js @@ -424,8 +424,10 @@ function renderState(state) { endpointBadge.textContent = endpointIsPassing ? "Pass" : "Fail"; endpointBadge.className = `endpoint-badge ${endpointIsPassing ? "passing" : "failing"}`; - endpointNode.querySelector(".response-body").textContent = - endpoint.responseBody || endpoint.errorMessage || "(empty response)"; + initializeResponsePreview( + endpointNode, + endpoint.responseBody || endpoint.errorMessage || "(empty response)" + ); const testList = endpointNode.querySelector(".test-list"); @@ -469,6 +471,33 @@ function renderState(state) { } } +function initializeResponsePreview(endpointNode, responseText) { + const responseBody = endpointNode.querySelector(".response-body"); + const formatButton = endpointNode.querySelector(".format-response-button"); + const toggleWrapButton = endpointNode.querySelector(".toggle-response-wrap-button"); + + responseBody.value = formatJsonText(responseText); + responseBody.dataset.rawValue = responseText; + responseBody.wrap = "soft"; + responseBody.classList.add("is-wrapped"); + responseBody.classList.remove("is-unwrapped"); + + toggleWrapButton.innerHTML = "Disable Wrap"; + toggleWrapButton.addEventListener("click", () => { + const isWrapped = responseBody.classList.contains("is-wrapped"); + responseBody.wrap = isWrapped ? "off" : "soft"; + responseBody.classList.toggle("is-wrapped", !isWrapped); + responseBody.classList.toggle("is-unwrapped", isWrapped); + toggleWrapButton.innerHTML = isWrapped + ? "Enable Wrap" + : "Disable Wrap"; + }); + + formatButton.addEventListener("click", () => { + responseBody.value = formatJsonText(responseBody.dataset.rawValue || responseBody.value); + }); +} + function filterRunEnvironments(run, searchTerm) { if (!searchTerm) { return run.environments.map((environment) => ({ @@ -615,6 +644,18 @@ function formatDate(value) { return new Date(value).toLocaleString(); } +function formatJsonText(value) { + if (typeof value !== "string") { + return String(value ?? ""); + } + + try { + return JSON.stringify(JSON.parse(value), null, 2); + } catch { + return value; + } +} + async function buildErrorMessage(response, fallbackMessage) { try { const payload = await response.json(); diff --git a/src/ApiTestRunner.App/wwwroot/curl-import.html b/src/ApiTestRunner.App/wwwroot/curl-import.html index 2e1a2dc..b41374e 100644 --- a/src/ApiTestRunner.App/wwwroot/curl-import.html +++ b/src/ApiTestRunner.App/wwwroot/curl-import.html @@ -100,7 +100,11 @@

Paste r
- +
+ + +
+

No response body parsed yet.

diff --git a/src/ApiTestRunner.App/wwwroot/curl-import.js b/src/ApiTestRunner.App/wwwroot/curl-import.js index a3ea18d..34abe24 100644 --- a/src/ApiTestRunner.App/wwwroot/curl-import.js +++ b/src/ApiTestRunner.App/wwwroot/curl-import.js @@ -4,6 +4,8 @@ const responseStatus = document.getElementById("responseStatus"); const addAssertionButton = document.getElementById("addAssertionButton"); const curlInput = document.getElementById("curlInput"); const responseBodyInput = document.getElementById("responseBodyInput"); +const formatResponseButton = document.getElementById("formatResponseButton"); +const toggleResponseWrapButton = document.getElementById("toggleResponseWrapButton"); const assertionFieldSelect = document.getElementById("assertionFieldSelect"); const assertionRuleSelect = document.getElementById("assertionRuleSelect"); const assertionValueContainer = document.getElementById("assertionValueContainer"); @@ -38,6 +40,7 @@ let parsedResponseFields = []; let parsedResponseObject = null; let assertionDrafts = []; let lastParsedResponseBody = ""; +let isResponseWrapped = true; async function analyzeCurlCommand() { const command = curlInput.value.trim(); @@ -120,6 +123,33 @@ function parseResponseBody() { } } +function formatResponseBody() { + const responseBody = responseBodyInput.value.trim(); + if (!responseBody) { + renderResponseStatus("Paste a response body first.", true); + return; + } + + try { + const parsed = JSON.parse(responseBody); + responseBodyInput.value = JSON.stringify(parsed, null, 2); + parseResponseBody(); + renderResponseStatus("Response body formatted.", false); + } catch (error) { + renderResponseStatus(error.message || "Response body is not valid JSON.", true); + } +} + +function toggleResponseWrap() { + isResponseWrapped = !isResponseWrapped; + responseBodyInput.wrap = isResponseWrapped ? "soft" : "off"; + responseBodyInput.classList.toggle("is-wrapped", isResponseWrapped); + responseBodyInput.classList.toggle("is-unwrapped", !isResponseWrapped); + toggleResponseWrapButton.innerHTML = isResponseWrapped + ? "Disable Wrap" + : "Enable Wrap"; +} + function collectResponseFields(value, path = "") { const fields = []; const type = getJsonValueType(value); @@ -617,6 +647,8 @@ function renderResponseStatus(message, isError) { function setBusy(isBusy) { analyzeButton.disabled = isBusy; addAssertionButton.disabled = isBusy || parsedResponseFields.length === 0; + formatResponseButton.disabled = isBusy; + toggleResponseWrapButton.disabled = isBusy; analyzeButton.innerHTML = isBusy ? "Analyzing..." : "Analyze and Generate"; @@ -653,6 +685,8 @@ assertionFieldSelect.addEventListener("change", () => { assertionRuleSelect.addEventListener("change", renderValueInput); addAssertionButton.addEventListener("click", addAssertionDraft); analyzeButton.addEventListener("click", analyzeCurlCommand); +formatResponseButton.addEventListener("click", formatResponseBody); +toggleResponseWrapButton.addEventListener("click", toggleResponseWrap); responseBodyInput.addEventListener("blur", parseResponseBody); renderAssertionBuilder(); diff --git a/src/ApiTestRunner.App/wwwroot/index.html b/src/ApiTestRunner.App/wwwroot/index.html index 88414ed..cf5baa9 100644 --- a/src/ApiTestRunner.App/wwwroot/index.html +++ b/src/ApiTestRunner.App/wwwroot/index.html @@ -242,7 +242,13 @@

Response preview -

+                    
+
+ + +
+ +
diff --git a/src/ApiTestRunner.App/wwwroot/styles.css b/src/ApiTestRunner.App/wwwroot/styles.css index 594b6f1..0f40bff 100644 --- a/src/ApiTestRunner.App/wwwroot/styles.css +++ b/src/ApiTestRunner.App/wwwroot/styles.css @@ -484,6 +484,11 @@ body.app-shell { padding: 0 0.9rem 0.9rem; } +.selection-group-body { + background: linear-gradient(180deg, rgba(226, 232, 240, 0.72), rgba(241, 245, 249, 0.88)); + border-top: 1px solid rgba(15, 23, 42, 0.08); +} + .selection-test-list, .endpoint-list, .test-list { @@ -501,8 +506,9 @@ body.app-shell { .selection-test { padding: 0.75rem 0.85rem; border-radius: 0.9rem; - background: rgba(255, 255, 255, 0.76); - border: 1px solid rgba(15, 23, 42, 0.08); + background: rgba(255, 255, 255, 0.99); + border: 1px solid rgba(15, 23, 42, 0.16); + box-shadow: 0 8px 18px rgba(15, 23, 42, 0.06); } .selection-test input, @@ -553,6 +559,15 @@ body.app-shell { background: rgba(255, 255, 255, 0.55); } +.selection-summary-row { + background: linear-gradient(180deg, rgba(226, 232, 240, 0.98), rgba(203, 213, 225, 0.92)); + border-bottom: 1px solid rgba(15, 23, 42, 0.12); +} + +.selection-subgroup > .selection-summary-row { + background: linear-gradient(180deg, rgba(241, 245, 249, 0.98), rgba(226, 232, 240, 0.94)); +} + .environment-card[open] > .environment-summary-row, .endpoint-card[open] > .endpoint-summary-row, .test-card[open] > .test-summary-row { @@ -634,19 +649,36 @@ body.app-shell { background: rgba(248, 250, 252, 0.88); } +.response-panel-body { + padding: 0.85rem 0.95rem 0.95rem; + border-top: 1px solid rgba(15, 23, 42, 0.08); +} + .response-body, .code-block { margin: 0; - padding: 0.85rem 0.95rem; overflow: auto; - border-top: 1px solid rgba(15, 23, 42, 0.08); background: #0f172a; color: #e2e8f0; - border-radius: 0 0 0.9rem 0.9rem; font-size: 0.81rem; line-height: 1.45; } +.response-body { + min-height: 16rem; + width: 100%; + padding: 0.85rem 0.95rem; + border: 0; + border-radius: 0.9rem; + resize: vertical; + font-family: Consolas, "SFMono-Regular", Menlo, monospace; +} + +.code-block { + padding: 0.85rem 0.95rem; + border-radius: 0 0 0.9rem 0.9rem; +} + .assertion-list { margin: 0; padding-left: 1.1rem; @@ -690,6 +722,25 @@ body.app-shell { line-height: 1.5; } +.response-preview-actions { + margin-bottom: 0.8rem; +} + +.response-preview-input { + min-height: 20rem; + font-size: 0.9rem; +} + +.response-preview-input.is-wrapped { + white-space: pre-wrap; + word-break: break-word; +} + +.response-preview-input.is-unwrapped { + white-space: pre; + overflow-x: auto; +} + .tool-input-inline, .tool-select { min-height: 2.9rem; diff --git a/src/ApiTestRunner.Core/Services/YamlTestSuiteLoader.cs b/src/ApiTestRunner.Core/Services/YamlTestSuiteLoader.cs index 07c6406..e0d41c8 100644 --- a/src/ApiTestRunner.Core/Services/YamlTestSuiteLoader.cs +++ b/src/ApiTestRunner.Core/Services/YamlTestSuiteLoader.cs @@ -52,6 +52,12 @@ public async Task LoadAsync(IEnumerable filePath var yaml = await File.ReadAllTextAsync(filePath, cancellationToken); var document = _deserializer.Deserialize(yaml) ?? new ApiTestDocumentDefinition(); + if (ShouldSkipEmptyDocument(yaml, document)) + { + _logger.LogWarning("Skipping empty YAML test file: {FilePath}", filePath); + continue; + } + ValidateDocumentShape(document, filePath); foreach (var environment in document.Environments) @@ -98,6 +104,25 @@ private static void ValidateDocumentShape(ApiTestDocumentDefinition document, st } } + private static bool ShouldSkipEmptyDocument(string yaml, ApiTestDocumentDefinition document) + { + if (document.Environments.Count > 0 || document.Endpoints.Count > 0) + { + return false; + } + + if (string.IsNullOrWhiteSpace(yaml)) + { + return true; + } + + var lines = yaml + .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()); + + return lines.All(line => line.Length == 0 || line == "---" || line == "..." || line.StartsWith('#')); + } + private static void ValidateEnvironment(EnvironmentDefinition environment, string filePath) { if (string.IsNullOrWhiteSpace(environment.Name)) diff --git a/tests/ApiTestRunner.Core.Tests/YamlTestSuiteLoaderTests.cs b/tests/ApiTestRunner.Core.Tests/YamlTestSuiteLoaderTests.cs index 65cbf0c..38059ff 100644 --- a/tests/ApiTestRunner.Core.Tests/YamlTestSuiteLoaderTests.cs +++ b/tests/ApiTestRunner.Core.Tests/YamlTestSuiteLoaderTests.cs @@ -81,6 +81,37 @@ public async Task LoadAsync_AttachesEndpointOnlyFileToSingleEnvironmentWhenTarge Assert.True(endpoint.Tests[0].Assertions[0].EqualsValue is bool boolean && boolean); } + [Fact] + public async Task LoadAsync_SkipsEmptyEndpointFileAndLoadsOtherConfiguredFiles() + { + var environmentFile = WriteYaml("environment.yaml", """ + environments: + - name: Local + baseUrl: https://localhost:7001 + """); + + var emptyEndpointFile = WriteYaml("empty-endpoint.yaml", ""); + + var endpointFile = WriteYaml("accounts.yaml", """ + targetEnvironments: + - Local + + endpoints: + - name: Get Accounts + method: GET + path: /api/accounts + tests: + - name: Accounts should exist + expectedStatus: 200 + """); + + var suite = await _loader.LoadAsync([environmentFile, emptyEndpointFile, endpointFile]); + + var environment = Assert.Single(suite.Environments); + var endpoint = Assert.Single(environment.Endpoints); + Assert.Equal("Get Accounts", endpoint.Name); + } + [Fact] public async Task LoadAsync_ThrowsForConflictingEnvironmentBaseUrls() { diff --git a/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.Tests.dll b/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.Tests.dll index 18ecb7a..0b287e1 100644 Binary files a/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.Tests.dll and b/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.Tests.dll differ diff --git a/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.Tests.pdb b/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.Tests.pdb index 30080e0..a95add8 100644 Binary files a/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.Tests.pdb and b/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.Tests.pdb differ diff --git a/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.dll b/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.dll index 04b3a01..7d60585 100644 Binary files a/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.dll and b/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.dll differ diff --git a/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.pdb b/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.pdb index a297b37..293c635 100644 Binary files a/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.pdb and b/tests/ApiTestRunner.Core.Tests/bin/Release/net8.0/ApiTestRunner.Core.pdb differ diff --git a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.AssemblyInfo.cs b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.AssemblyInfo.cs index 619a1ea..5af6ebe 100644 --- a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.AssemblyInfo.cs +++ b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.AssemblyInfo.cs @@ -13,7 +13,7 @@ [assembly: System.Reflection.AssemblyCompanyAttribute("ApiTestRunner.Core.Tests")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+b6531e3d6db2e3250c965f806f08cd3f385db4fb")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+63041666a7071e4da20206b2c691c0ad24079ce6")] [assembly: System.Reflection.AssemblyProductAttribute("ApiTestRunner.Core.Tests")] [assembly: System.Reflection.AssemblyTitleAttribute("ApiTestRunner.Core.Tests")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.AssemblyInfoInputs.cache b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.AssemblyInfoInputs.cache index 53d95c4..2631508 100644 --- a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.AssemblyInfoInputs.cache +++ b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.AssemblyInfoInputs.cache @@ -1 +1 @@ -6bf5c1731d5b5f4edbe1a68420688a78362680f6b716751ae68a391bafad5036 +836548df8afe09565129f3173b86cff614d1d691aea07ae448f6a0c8fbe5591e diff --git a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.csproj.AssemblyReference.cache b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.csproj.AssemblyReference.cache index 0c21c4f..fc9d4da 100644 Binary files a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.csproj.AssemblyReference.cache and b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.csproj.AssemblyReference.cache differ diff --git a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.dll b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.dll index 18ecb7a..0b287e1 100644 Binary files a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.dll and b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.dll differ diff --git a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.pdb b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.pdb index 30080e0..a95add8 100644 Binary files a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.pdb and b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.pdb differ diff --git a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.sourcelink.json b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.sourcelink.json index c835c59..cb71235 100644 --- a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.sourcelink.json +++ b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ApiTestRunner.Core.Tests.sourcelink.json @@ -1 +1 @@ -{"documents":{"D:\\Projects\\Research\\EndpointTestRunner\\*":"https://raw.githubusercontent.com/javaChip56/EndpointTestRunner/b6531e3d6db2e3250c965f806f08cd3f385db4fb/*"}} \ No newline at end of file +{"documents":{"D:\\Projects\\Research\\EndpointTestRunner\\*":"https://raw.githubusercontent.com/javaChip56/EndpointTestRunner/63041666a7071e4da20206b2c691c0ad24079ce6/*"}} \ No newline at end of file diff --git a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ref/ApiTestRunner.Core.Tests.dll b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ref/ApiTestRunner.Core.Tests.dll index 38449a9..5cf66d4 100644 Binary files a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ref/ApiTestRunner.Core.Tests.dll and b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/ref/ApiTestRunner.Core.Tests.dll differ diff --git a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/refint/ApiTestRunner.Core.Tests.dll b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/refint/ApiTestRunner.Core.Tests.dll index 38449a9..5cf66d4 100644 Binary files a/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/refint/ApiTestRunner.Core.Tests.dll and b/tests/ApiTestRunner.Core.Tests/obj/Release/net8.0/refint/ApiTestRunner.Core.Tests.dll differ