From b6531e3d6db2e3250c965f806f08cd3f385db4fb Mon Sep 17 00:00:00 2001 From: gali Date: Sun, 15 Mar 2026 19:08:44 +0800 Subject: [PATCH 1/2] feat: Add search functionality for APIs and endpoints in selection tree, enhance UI with search input and styles --- README.md | 1 + src/ApiTestRunner.App/wwwroot/app.js | 98 ++++++++++++++++-- src/ApiTestRunner.App/wwwroot/index.html | 6 ++ src/ApiTestRunner.App/wwwroot/styles.css | 32 ++++++ .../net8.0/ApiTestRunner.Core.Tests.dll | Bin 29184 -> 29184 bytes .../net8.0/ApiTestRunner.Core.Tests.pdb | Bin 25312 -> 25308 bytes .../bin/Release/net8.0/ApiTestRunner.Core.dll | Bin 70144 -> 70144 bytes .../bin/Release/net8.0/ApiTestRunner.Core.pdb | Bin 37580 -> 37576 bytes .../ApiTestRunner.Core.Tests.AssemblyInfo.cs | 2 +- ...Runner.Core.Tests.AssemblyInfoInputs.cache | 2 +- ....Core.Tests.csproj.AssemblyReference.cache | Bin 10666 -> 10666 bytes .../net8.0/ApiTestRunner.Core.Tests.dll | Bin 29184 -> 29184 bytes .../net8.0/ApiTestRunner.Core.Tests.pdb | Bin 25312 -> 25308 bytes .../ApiTestRunner.Core.Tests.sourcelink.json | 2 +- .../net8.0/ref/ApiTestRunner.Core.Tests.dll | Bin 14336 -> 14336 bytes .../refint/ApiTestRunner.Core.Tests.dll | Bin 14336 -> 14336 bytes 16 files changed, 133 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 464c518..280eb6f 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ The main dashboard now exposes a test-selection panel before execution: - `Select All` enables the full suite. - `Clear All` disables every test. - `Expand All` and `Collapse All` control the selection tree. +- Search filters APIs, base URLs, endpoints, methods, and test names in the selection tree. - Environment, endpoint, and individual test checkboxes let you run only the subset you care about. - Each page load starts with all tests selected by default. - The dashboard and cURL import pages use local AdminLTE assets, so the UI does not rely on external CDNs at runtime. diff --git a/src/ApiTestRunner.App/wwwroot/app.js b/src/ApiTestRunner.App/wwwroot/app.js index 99a2cb7..b519c79 100644 --- a/src/ApiTestRunner.App/wwwroot/app.js +++ b/src/ApiTestRunner.App/wwwroot/app.js @@ -6,6 +6,7 @@ const expandSelectionButton = document.getElementById("expandSelectionButton"); const collapseSelectionButton = document.getElementById("collapseSelectionButton"); const expandResultsButton = document.getElementById("expandResultsButton"); const collapseResultsButton = document.getElementById("collapseResultsButton"); +const selectionSearchInput = document.getElementById("selectionSearchInput"); const selectionContainer = document.getElementById("selectionContainer"); const selectionSummary = document.getElementById("selectionSummary"); const resultsSummary = document.getElementById("resultsSummary"); @@ -17,6 +18,7 @@ const testTemplate = document.getElementById("testTemplate"); let suiteManifest = null; let selectedTestIds = new Set(); let lastRunState = null; +let selectionSearchTerm = ""; const selectionExpansionState = { environments: new Map(), @@ -116,6 +118,7 @@ function hydrateSelection(manifest) { function renderSelection(manifest) { selectionContainer.innerHTML = ""; + const selectionSearchDisplayTerm = selectionSearchInput.value.trim(); if (!manifest || !manifest.environments || manifest.environments.length === 0) { selectionSummary.textContent = "No tests were found in the configured YAML files."; @@ -125,14 +128,29 @@ function renderSelection(manifest) { } const totalTestCount = manifest.totalTests; - selectionSummary.textContent = `${selectedTestIds.size} of ${totalTestCount} tests selected`; + const filteredEnvironments = filterManifestEnvironments(manifest, selectionSearchTerm); + 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.endpoint.tests.length, 0), + 0 + ); + + selectionSummary.textContent = selectionSearchTerm + ? `${selectedTestIds.size} of ${totalTestCount} tests selected • ${visibleEndpointCount} endpoints and ${visibleTestCount} tests shown for "${selectionSearchDisplayTerm}"` + : `${selectedTestIds.size} of ${totalTestCount} tests selected`; updateSelectionButtons(true); - for (const environment of manifest.environments) { - const environmentIds = environment.endpoints.flatMap((endpoint) => endpoint.tests.map((test) => test.id)); + if (filteredEnvironments.length === 0) { + selectionContainer.innerHTML = `

No APIs or endpoints match "${escapeHtml(selectionSearchDisplayTerm)}".

`; + return; + } + + for (const environmentEntry of filteredEnvironments) { + const { environment, endpoints, environmentMatches } = environmentEntry; + const environmentIds = endpoints.flatMap((endpointEntry) => endpointEntry.endpoint.tests.map((test) => test.id)); const environmentNode = document.createElement("details"); environmentNode.className = "selection-group"; - environmentNode.open = selectionExpansionState.environments.get(environment.id) ?? true; + environmentNode.open = selectionSearchTerm ? true : selectionExpansionState.environments.get(environment.id) ?? true; environmentNode.addEventListener("toggle", () => { selectionExpansionState.environments.set(environment.id, environmentNode.open); }); @@ -142,7 +160,9 @@ function renderSelection(manifest) { const environmentHeader = createSelectionHeader( environment.name, - `${environment.baseUrl} - ${environment.totalTests} tests`, + environmentMatches || !selectionSearchTerm + ? `${environment.baseUrl} - ${environment.totalTests} tests` + : `${environment.baseUrl} - ${environmentIds.length} matching tests`, environmentIds, toggleGroupSelection ); @@ -153,11 +173,12 @@ function renderSelection(manifest) { const environmentBody = document.createElement("div"); environmentBody.className = "selection-group-body"; - for (const endpoint of environment.endpoints) { + for (const endpointEntry of endpoints) { + const { endpoint } = endpointEntry; const endpointIds = endpoint.tests.map((test) => test.id); const endpointNode = document.createElement("details"); endpointNode.className = "selection-subgroup"; - endpointNode.open = selectionExpansionState.endpoints.get(endpoint.id) ?? false; + endpointNode.open = selectionSearchTerm ? true : selectionExpansionState.endpoints.get(endpoint.id) ?? false; endpointNode.addEventListener("toggle", () => { selectionExpansionState.endpoints.set(endpoint.id, endpointNode.open); }); @@ -255,6 +276,62 @@ function getAllTestIds(manifest) { ); } +function filterManifestEnvironments(manifest, searchTerm) { + if (!searchTerm) { + return manifest.environments.map((environment) => ({ + environment, + environmentMatches: false, + endpoints: environment.endpoints.map((endpoint) => ({ + endpoint, + endpointMatches: false + })) + })); + } + + return manifest.environments + .map((environment) => { + const environmentMatches = + matchesSelectionSearch(environment.name, searchTerm) || + matchesSelectionSearch(environment.baseUrl, searchTerm); + + const visibleEndpoints = environment.endpoints + .filter((endpoint) => environmentMatches || endpointMatchesSelectionSearch(endpoint, searchTerm)) + .map((endpoint) => ({ + endpoint, + endpointMatches: environmentMatches || endpointMatchesSelectionSearch(endpoint, searchTerm) + })); + + if (!environmentMatches && visibleEndpoints.length === 0) { + return null; + } + + return { + environment, + environmentMatches, + endpoints: environmentMatches + ? environment.endpoints.map((endpoint) => ({ + endpoint, + endpointMatches: true + })) + : visibleEndpoints + }; + }) + .filter((entry) => entry !== null); +} + +function endpointMatchesSelectionSearch(endpoint, searchTerm) { + return ( + matchesSelectionSearch(endpoint.name, searchTerm) || + matchesSelectionSearch(endpoint.method, searchTerm) || + matchesSelectionSearch(endpoint.path, searchTerm) || + endpoint.tests.some((test) => matchesSelectionSearch(test.name, searchTerm)) + ); +} + +function matchesSelectionSearch(value, searchTerm) { + return typeof value === "string" && value.toLowerCase().includes(searchTerm); +} + function renderState(state) { lastRunState = state; const run = state.lastRun; @@ -568,5 +645,12 @@ expandSelectionButton.addEventListener("click", () => setSelectionExpansion(true collapseSelectionButton.addEventListener("click", () => setSelectionExpansion(false)); expandResultsButton.addEventListener("click", () => setResultExpansion(true)); collapseResultsButton.addEventListener("click", () => setResultExpansion(false)); +selectionSearchInput.addEventListener("input", () => { + selectionSearchTerm = selectionSearchInput.value.trim().toLowerCase(); + + if (suiteManifest) { + renderSelection(suiteManifest); + } +}); initializeDashboard(); diff --git a/src/ApiTestRunner.App/wwwroot/index.html b/src/ApiTestRunner.App/wwwroot/index.html index 4c34dc5..b32203a 100644 --- a/src/ApiTestRunner.App/wwwroot/index.html +++ b/src/ApiTestRunner.App/wwwroot/index.html @@ -164,6 +164,12 @@

T
+
+ +
diff --git a/src/ApiTestRunner.App/wwwroot/styles.css b/src/ApiTestRunner.App/wwwroot/styles.css index 6b4fe0a..2e821a5 100644 --- a/src/ApiTestRunner.App/wwwroot/styles.css +++ b/src/ApiTestRunner.App/wwwroot/styles.css @@ -228,6 +228,38 @@ body.app-shell { align-items: center; } +.selection-search-row { + margin-bottom: 0.9rem; +} + +.selection-search-input { + display: flex; + align-items: center; + gap: 0.7rem; + width: 100%; + padding: 0.75rem 0.9rem; + border-radius: 0.9rem; + border: 1px solid rgba(15, 23, 42, 0.1); + background: rgba(255, 255, 255, 0.82); + color: #64748b; +} + +.selection-search-input i { + color: #64748b; +} + +.selection-search-input input { + width: 100%; + border: 0; + background: transparent; + color: #0f172a; + outline: none; +} + +.selection-search-input input::placeholder { + color: #94a3b8; +} + .primary-button, .ghost-button { display: inline-flex; 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 198cdac6ad41edc8e697bbf79848fa773da84561..debdb93f8f3fa69b6e000117ed7ff9e74b47d229 100644 GIT binary patch delta 252 zcmZp8!r1VHaY6^nnN;f2$iR@z$dER9LbBv!*;KL3e#v=E zERQbMEt-5FH9+8S_@xa!efRz6-})WS9eJg5vO-#u0#vX+9x4b_tz`3HhQM>12?y@% z?>@HqP}(;Ze{%+Nh9m}KhBSs0AZY;PnKGC$qyotlAT|V&hCopRhD0DVV6X&|7C`lh ZKo&@CG7zSK^&0?Lh75_D*>irf0s!ddRx1Di delta 252 zcmZp8!r1VHaY6@+`b+OM8++D73LKr}`y=KRi~GLML6g$w-h02zv5}#rQDTyrkwsc^qD5korKyRDp`med5(7gvBSYHc3CWU^WmCmA`z7Zw zu_Oo`S~2-RYJfnYJ+oh@M3Z0RWaCp)EdH@iR!D17fC|PPgbIRG|7Lrzd+Q;aF6UH6 zqs@oXzOne5F_A=a(@Ek~TFno<{0J8m}4gkr{C?8SUr#j#69)Zp8 zMNS+8|+xymk>=!Q|%a|0jYfZLfNo$`h z+hX0@iEkF%@X9K9lccTv^ZVhu9J;?1+Ha_zTt6?IL%Xki?z}qvzQjJ!+lh{CGDatN zCmYRq>sKidCM0vgS-HMp4I9h0Fn7mWC#GAQuvR8;%V-rZ<@8$PnDCTyir_U%As)%M zADkoEGgM@zT$>=6@mezE-h~SH_Oax$PHc z75O$zHQlUG>->J@nUdPy!2-Jb?oZoxXYoq$Gk&R}myafF43GY_XI+n?c)jl0#TrNI z3Vz<3m01)T!sq<0WsjQgiWFrBymS$&mx8g3=EB}EN3fuo_Y7Qt+zW;@V0XD zMq#nd{em7oDt?Ry7(zoqL;;AX0ugN>Vj7584kGqV{vEr`ofw56)o($>4S$T#6 zCErQdjN3&5Jv!U0uUDgaa9D&07ix;AioQUCjs#+B=JRXHb^ZK!{kUNhFL(oFcQRP zW>^`?%&-B<<^XyOBo+svSr`}@W^SI%RLQJs2bO1KI2Ovtupo>XEXlyY3Zxksc*EI& zGzSoKg83IGJF=tXn9NxT@O_;6z*`F0LilJ_&J>ToOHT!R&1=*i6mX$ zS(AU}%1)g`St@jYL%HoUia@9|{tEtvm_0a6N&8s^%K3(8lWnU3~U0G!E!;~q3 z`a320+5Vt9O&9#epD%w`sAOHW9+xPA8$62ka z6;b!AR~l7pos^kq6cwf8b&1DmVR0byir*}g-S`?c%uExLEln&k4gaJ0fx|U5K#moYCuE>h?oH)R)UBFlYho;b0I2(XwwW*C91{}*0|2UJ>69uXd+~($4_M(Tm$ktcW zXnPJf;|*qia|UyUBnD%KG=>x)X#nJzGMF)>0?8C0HUyG}Kv4sRL?AR^umqA8K=p}0 Z7D#O}5T=0j8vt2`42jz(^E2i$0RRsYP9XpQ delta 254 zcmZoz!qTvWWkLtb7x&)V8+-n|7m%yE#I|u^g7YS^(5@5A1@AY{_^?@A&&)J2+0w+) zG$qx*(A>h<*vQb*C^5;*$RaH{(IPR)($vJn(9k$JiGjhMk-=(u0w<%)_Ng3<_ZeBr zh0AA6ui#<~5SaY#hH$?4XV0D=b~5)eCvKR2gNw050V-;F1}X|t&)0M8m?vZU-p?Cq zrfkpQX1u}dZ^mHCkjRkCV98(t#HI`>45FN=KxhPHTLQ^Mh9m|vAZ@{r c22`C0q(LGe9VS3%2!zH!@ucmO`5AMW0LCp(9smFU 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 55bea81dc4299acecd4a87b4e88bf8ba4f90367a..7a0e0c3aeed9ec28ad515d08477cd3c0f48d0bd8 100644 GIT binary patch delta 2458 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 F{sGh_fr9`5 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 6a8fda2..f8b6d5d 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+65ac9495de0178332192ab628fca8ab9544113cb")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+77b3fd30756e5d51d100aa09a8ba907ccdb30c1a")] [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 3ed580d..41dd418 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 @@ -97c02c74a7fbd12e17e64b87f8cfa9ae138c3c19a7df23cb9ddfd960b4de5054 +77a93abd55010f715413cd1e4a89aaa5c8a63491782c86e554b5e3ffec8ba3fa 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 ab809ded06d223c68ce885a863c84a817c996b19..073ab920b532fe6eab2541b05cb48a2813b987a8 100644 GIT binary patch delta 17 YcmZ1#yefD?DO<)R%ZjF^jn(>^07mTxc>n+a delta 17 YcmZ1#yefD?DchlX&eZFT8>{s-0Y`cVkpKVy 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 198cdac6ad41edc8e697bbf79848fa773da84561..debdb93f8f3fa69b6e000117ed7ff9e74b47d229 100644 GIT binary patch delta 252 zcmZp8!r1VHaY6^nnN;f2$iR@z$dER9LbBv!*;KL3e#v=E zERQbMEt-5FH9+8S_@xa!efRz6-})WS9eJg5vO-#u0#vX+9x4b_tz`3HhQM>12?y@% z?>@HqP}(;Ze{%+Nh9m}KhBSs0AZY;PnKGC$qyotlAT|V&hCopRhD0DVV6X&|7C`lh ZKo&@CG7zSK^&0?Lh75_D*>irf0s!ddRx1Di delta 252 zcmZp8!r1VHaY6@+`b+OM8++D73LKr}`y=KRi~GLML6g$w-h02zv5}#rQDTyrkwsc^qD5korKyRDp`med5(7gvBSYHc3CWU^WmCmA`z7Zw zu_Oo`S~2-RYJfnYJ+oh@M3Z0RWaCp)EdH@iR!D17fC|PPgbIRG|7Lrzd+Q;aF6UH6 zqs@oXzOne5F_A=a(@Ek~TFno<{0J8m}4gkr{C?8SUr#j#69)Zp8 zMNS+8|+xymk>=!Q|%a|0jYfZLfNo$`h z+hX0@iEkF%@X9K9lccTv^ZVhu9J;?1+Ha_zTt6?IL%Xki?z}qvzQjJ!+lh{CGDatN zCmYRq>sKidCM0vgS-HMp4I9h0Fn7mWC#GAQuvR8;%V-rZ<@8$PnDCTyir_U%As)%M zADkoEGgM@zT$>=6@mezE-h~SH_Oax$PHc z75O$zHQlUG>->J@nUdPy!2-Jb?oZoxXYoq$Gk&R}myafF43GY_XI+n?c)jl0#TrNI z3Vz<3m01)T!sq<0WsjQgiWFrBymS$&mx8g3=EB}EN3fuo_Y7Qt+zW;@V0XD zMq#nd{em7oDt?Ry7(zoqL;;AX0ugN>Vj7584kGqV{vEr`ofw56)o($>4S$T#6 zCErQdjN3&5Jv!U0uUDgaa9D&07ix;AioQUCjs#+B=JRXHb^ZK!{kUNhFL(oFcQRP zW>^`?%&-B<<^XyOBo+svSr`}@W^SI%RLQJs2bO1KI2Ovtupo>XEXlyY3Zxksc*EI& zGzSoKg83IGJF=tXn9NxT@O_;6z*`F0LilJ_&J>ToOHT!R&1=*i6mX$ zS(AU}%1)g`St@jYL%HoUia@9|{tEtvm_0a6N&8s^%K3(8lWnU3~U0G!E!;~q3 z`a320+5Vt9O&9#epD%w`sAOHW9+xPA8$62ka z6;b!AR~l7pos^kq6cwf8b&1DmVR0byir*}g-S`?c%uExLEln&k4gaJ0fx|U5K#moYCuE>h?oH)R)UBFlYho;b0I2(XwwW*C91{}*0|2UK`}!W9uus*&n*~|kGj9IRTC0dD)uTRv zNzdFo$v7>=*udP>EY&o{)G)=+z#uWvz%tPyDbdovJUKZf$=D#-Fp+^Fn~@=Hvc9(D z=47o+_6d-8;?6d*PPl7>K01BOH(G+?jXX<%o!{gjDgSy$hHKMi3~{$WK`}!W9uus*&n*~|kGj9IRTC0dD)uTRv zNzdFo$v7>=*udP>EY&o{)G)=+z#uWvz%tPyDbdovJUKZf$=D#-Fp+^Fn~@=Hvc9(D z=47o+_6d-8;?6d*PPl7>K01BOH(G+?jXX<%o!{gjDgSy$hHKMi3~{$W Date: Sun, 15 Mar 2026 19:20:00 +0800 Subject: [PATCH 2/2] feat: Implement live search functionality for results view, enhancing user experience with highlighted matches --- README.md | 2 + src/ApiTestRunner.App/wwwroot/app.js | 273 +++++++++++++----- src/ApiTestRunner.App/wwwroot/index.html | 8 + src/ApiTestRunner.App/wwwroot/styles.css | 7 + .../net8.0/ApiTestRunner.Core.Tests.dll | Bin 29184 -> 29184 bytes .../net8.0/ApiTestRunner.Core.Tests.pdb | Bin 25308 -> 25308 bytes .../bin/Release/net8.0/ApiTestRunner.Core.dll | Bin 70144 -> 70144 bytes .../bin/Release/net8.0/ApiTestRunner.Core.pdb | Bin 37576 -> 37576 bytes .../ApiTestRunner.Core.Tests.AssemblyInfo.cs | 2 +- ...Runner.Core.Tests.AssemblyInfoInputs.cache | 2 +- ....Core.Tests.csproj.AssemblyReference.cache | Bin 10666 -> 10666 bytes .../net8.0/ApiTestRunner.Core.Tests.dll | Bin 29184 -> 29184 bytes .../net8.0/ApiTestRunner.Core.Tests.pdb | Bin 25308 -> 25308 bytes .../ApiTestRunner.Core.Tests.sourcelink.json | 2 +- .../net8.0/ref/ApiTestRunner.Core.Tests.dll | Bin 14336 -> 14336 bytes .../refint/ApiTestRunner.Core.Tests.dll | Bin 14336 -> 14336 bytes 16 files changed, 217 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 280eb6f..43c5afe 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ The main dashboard now exposes a test-selection panel before execution: - `Clear All` disables every test. - `Expand All` and `Collapse All` control the selection tree. - Search filters APIs, base URLs, endpoints, methods, and test names in the selection tree. +- The results view also has live search for APIs, endpoints, tests, assertions, and error text. +- Matching text is highlighted in both selection and result sections to make hits easier to spot. - Environment, endpoint, and individual test checkboxes let you run only the subset you care about. - Each page load starts with all tests selected by default. - The dashboard and cURL import pages use local AdminLTE assets, so the UI does not rely on external CDNs at runtime. diff --git a/src/ApiTestRunner.App/wwwroot/app.js b/src/ApiTestRunner.App/wwwroot/app.js index b519c79..5d800d8 100644 --- a/src/ApiTestRunner.App/wwwroot/app.js +++ b/src/ApiTestRunner.App/wwwroot/app.js @@ -7,6 +7,7 @@ const collapseSelectionButton = document.getElementById("collapseSelectionButton const expandResultsButton = document.getElementById("expandResultsButton"); const collapseResultsButton = document.getElementById("collapseResultsButton"); const selectionSearchInput = document.getElementById("selectionSearchInput"); +const resultsSearchInput = document.getElementById("resultsSearchInput"); const selectionContainer = document.getElementById("selectionContainer"); const selectionSummary = document.getElementById("selectionSummary"); const resultsSummary = document.getElementById("resultsSummary"); @@ -19,6 +20,7 @@ let suiteManifest = null; let selectedTestIds = new Set(); let lastRunState = null; let selectionSearchTerm = ""; +let resultsSearchTerm = ""; const selectionExpansionState = { environments: new Map(), @@ -118,7 +120,7 @@ function hydrateSelection(manifest) { function renderSelection(manifest) { selectionContainer.innerHTML = ""; - const selectionSearchDisplayTerm = selectionSearchInput.value.trim(); + const searchDisplayTerm = selectionSearchInput.value.trim(); if (!manifest || !manifest.environments || manifest.environments.length === 0) { selectionSummary.textContent = "No tests were found in the configured YAML files."; @@ -131,23 +133,24 @@ function renderSelection(manifest) { const filteredEnvironments = filterManifestEnvironments(manifest, selectionSearchTerm); 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.endpoint.tests.length, 0), + (count, environmentEntry) => count + environmentEntry.endpoints.reduce((endpointCount, endpointEntry) => endpointCount + endpointEntry.tests.length, 0), 0 ); selectionSummary.textContent = selectionSearchTerm - ? `${selectedTestIds.size} of ${totalTestCount} tests selected • ${visibleEndpointCount} endpoints and ${visibleTestCount} tests shown for "${selectionSearchDisplayTerm}"` + ? `${selectedTestIds.size} of ${totalTestCount} tests selected | ${visibleEndpointCount} endpoints and ${visibleTestCount} tests shown for "${searchDisplayTerm}"` : `${selectedTestIds.size} of ${totalTestCount} tests selected`; updateSelectionButtons(true); if (filteredEnvironments.length === 0) { - selectionContainer.innerHTML = `

No APIs or endpoints match "${escapeHtml(selectionSearchDisplayTerm)}".

`; + selectionContainer.innerHTML = `

No APIs or endpoints match "${escapeHtml(searchDisplayTerm)}".

`; return; } for (const environmentEntry of filteredEnvironments) { - const { environment, endpoints, environmentMatches } = environmentEntry; - const environmentIds = endpoints.flatMap((endpointEntry) => endpointEntry.endpoint.tests.map((test) => test.id)); + const { environment, environmentMatches, endpoints } = environmentEntry; + const visibleEnvironmentTestIds = endpoints.flatMap((endpointEntry) => endpointEntry.tests.map((test) => test.id)); + const environmentNode = document.createElement("details"); environmentNode.className = "selection-group"; environmentNode.open = selectionSearchTerm ? true : selectionExpansionState.environments.get(environment.id) ?? true; @@ -157,25 +160,22 @@ function renderSelection(manifest) { const environmentSummary = document.createElement("summary"); environmentSummary.className = "selection-summary-row"; - - const environmentHeader = createSelectionHeader( - environment.name, + environmentSummary.appendChild(createSelectionHeader( + highlightMatch(environment.name, selectionSearchTerm), environmentMatches || !selectionSearchTerm - ? `${environment.baseUrl} - ${environment.totalTests} tests` - : `${environment.baseUrl} - ${environmentIds.length} matching tests`, - environmentIds, + ? `${highlightMatch(environment.baseUrl, selectionSearchTerm)} - ${environment.totalTests} tests` + : `${highlightMatch(environment.baseUrl, selectionSearchTerm)} - ${visibleEnvironmentTestIds.length} matching tests`, + visibleEnvironmentTestIds, toggleGroupSelection - ); - - environmentSummary.appendChild(environmentHeader); - environmentNode.appendChild(environmentSummary); + )); const environmentBody = document.createElement("div"); environmentBody.className = "selection-group-body"; for (const endpointEntry of endpoints) { - const { endpoint } = endpointEntry; - const endpointIds = endpoint.tests.map((test) => test.id); + const { endpoint, endpointMatches, tests } = endpointEntry; + const endpointIds = tests.map((test) => test.id); + const endpointNode = document.createElement("details"); endpointNode.className = "selection-subgroup"; endpointNode.open = selectionSearchTerm ? true : selectionExpansionState.endpoints.get(endpoint.id) ?? false; @@ -185,21 +185,19 @@ function renderSelection(manifest) { const endpointSummary = document.createElement("summary"); endpointSummary.className = "selection-summary-row"; - - const endpointHeader = createSelectionHeader( - endpoint.name, - `${endpoint.method} ${endpoint.path} - ${endpoint.tests.length} tests`, + endpointSummary.appendChild(createSelectionHeader( + highlightMatch(endpoint.name, selectionSearchTerm), + endpointMatches || !selectionSearchTerm + ? `${highlightMatch(endpoint.method, selectionSearchTerm)} ${highlightMatch(endpoint.path, selectionSearchTerm)} - ${tests.length} tests` + : `${highlightMatch(endpoint.method, selectionSearchTerm)} ${highlightMatch(endpoint.path, selectionSearchTerm)} - ${tests.length} matching tests`, endpointIds, toggleGroupSelection - ); - - endpointSummary.appendChild(endpointHeader); - endpointNode.appendChild(endpointSummary); + )); const testList = document.createElement("div"); testList.className = "selection-test-list"; - for (const test of endpoint.tests) { + for (const test of tests) { const testRow = document.createElement("label"); testRow.className = "selection-test"; @@ -210,29 +208,31 @@ function renderSelection(manifest) { const details = document.createElement("span"); details.className = "selection-label-stack"; - details.innerHTML = `${escapeHtml(test.name)}Expected HTTP ${test.expectedStatus}`; + details.innerHTML = `${highlightMatch(test.name, selectionSearchTerm)}Expected HTTP ${test.expectedStatus}`; testRow.appendChild(checkbox); testRow.appendChild(details); testList.appendChild(testRow); } + endpointNode.appendChild(endpointSummary); endpointNode.appendChild(testList); environmentBody.appendChild(endpointNode); } + environmentNode.appendChild(environmentSummary); environmentNode.appendChild(environmentBody); selectionContainer.appendChild(environmentNode); } } -function createSelectionHeader(title, detail, childTestIds, onToggle) { +function createSelectionHeader(titleHtml, detailHtml, childTestIds, onToggle) { const header = document.createElement("div"); header.className = "selection-header-row"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; - checkbox.checked = childTestIds.every((testId) => selectedTestIds.has(testId)); + checkbox.checked = childTestIds.length > 0 && childTestIds.every((testId) => selectedTestIds.has(testId)); checkbox.indeterminate = !checkbox.checked && childTestIds.some((testId) => selectedTestIds.has(testId)); checkbox.addEventListener("click", (event) => { event.stopPropagation(); @@ -241,7 +241,7 @@ function createSelectionHeader(title, detail, childTestIds, onToggle) { const labelStack = document.createElement("span"); labelStack.className = "selection-label-stack"; - labelStack.innerHTML = `${escapeHtml(title)}${escapeHtml(detail)}`; + labelStack.innerHTML = `${titleHtml}${detailHtml}`; header.appendChild(checkbox); header.appendChild(labelStack); @@ -283,7 +283,8 @@ function filterManifestEnvironments(manifest, searchTerm) { environmentMatches: false, endpoints: environment.endpoints.map((endpoint) => ({ endpoint, - endpointMatches: false + endpointMatches: false, + tests: endpoint.tests })) })); } @@ -291,29 +292,30 @@ function filterManifestEnvironments(manifest, searchTerm) { return manifest.environments .map((environment) => { const environmentMatches = - matchesSelectionSearch(environment.name, searchTerm) || - matchesSelectionSearch(environment.baseUrl, searchTerm); + matchesSearch(environment.name, searchTerm) || + matchesSearch(environment.baseUrl, searchTerm); - const visibleEndpoints = environment.endpoints - .filter((endpoint) => environmentMatches || endpointMatchesSelectionSearch(endpoint, searchTerm)) - .map((endpoint) => ({ - endpoint, - endpointMatches: environmentMatches || endpointMatchesSelectionSearch(endpoint, searchTerm) - })); - - if (!environmentMatches && visibleEndpoints.length === 0) { + const endpoints = environment.endpoints + .map((endpoint) => { + const endpointMatches = environmentMatches || endpointMatchesSelectionSearch(endpoint, searchTerm); + return { + endpoint, + endpointMatches, + tests: endpointMatches + ? endpoint.tests + : endpoint.tests.filter((test) => testMatchesSelectionSearch(test, searchTerm)) + }; + }) + .filter((endpointEntry) => endpointEntry.endpointMatches || endpointEntry.tests.length > 0); + + if (!environmentMatches && endpoints.length === 0) { return null; } return { environment, environmentMatches, - endpoints: environmentMatches - ? environment.endpoints.map((endpoint) => ({ - endpoint, - endpointMatches: true - })) - : visibleEndpoints + endpoints }; }) .filter((entry) => entry !== null); @@ -321,20 +323,20 @@ function filterManifestEnvironments(manifest, searchTerm) { function endpointMatchesSelectionSearch(endpoint, searchTerm) { return ( - matchesSelectionSearch(endpoint.name, searchTerm) || - matchesSelectionSearch(endpoint.method, searchTerm) || - matchesSelectionSearch(endpoint.path, searchTerm) || - endpoint.tests.some((test) => matchesSelectionSearch(test.name, searchTerm)) + matchesSearch(endpoint.name, searchTerm) || + matchesSearch(endpoint.method, searchTerm) || + matchesSearch(endpoint.path, searchTerm) ); } -function matchesSelectionSearch(value, searchTerm) { - return typeof value === "string" && value.toLowerCase().includes(searchTerm); +function testMatchesSelectionSearch(test, searchTerm) { + return matchesSearch(test.name, searchTerm); } function renderState(state) { lastRunState = state; const run = state.lastRun; + const searchDisplayTerm = resultsSearchInput.value.trim(); setStatusError(Boolean(state.lastError)); document.getElementById("runStatus").textContent = buildStatusText(state); @@ -359,59 +361,83 @@ function renderState(state) { return; } - resultsSummary.textContent = `${run.passedTests} passed, ${run.failedTests} failed across ${run.environments.length} environments.`; + const filteredEnvironments = filterRunEnvironments(run, resultsSearchTerm); + 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.`; updateResultButtons(true); synchronizeResultExpansionState(run); - for (const environment of run.environments) { + if (filteredEnvironments.length === 0) { + environmentContainer.innerHTML = `

No matching results

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

`; + return; + } + + for (const environmentEntry of filteredEnvironments) { + const { environment, environmentMatches, endpoints } = environmentEntry; const environmentKey = getResultEnvironmentKey(environment); + const visibleTests = endpoints.flatMap((endpointEntry) => endpointEntry.tests); + const visiblePassedTests = visibleTests.filter((test) => test.isSuccess).length; + const visibleFailedTests = visibleTests.length - visiblePassedTests; + const environmentNode = environmentTemplate.content.firstElementChild.cloneNode(true); - environmentNode.open = resultExpansionState.environments.get(environmentKey) ?? environment.failedTests > 0; + environmentNode.open = resultsSearchTerm ? true : resultExpansionState.environments.get(environmentKey) ?? environment.failedTests > 0; environmentNode.addEventListener("toggle", () => { resultExpansionState.environments.set(environmentKey, environmentNode.open); }); - environmentNode.querySelector(".environment-name").textContent = environment.name; - environmentNode.querySelector(".environment-url").textContent = environment.baseUrl; - environmentNode.querySelector(".environment-stats").textContent = - `${environment.passedTests} passed, ${environment.failedTests} failed, ${environment.totalTests} total`; + 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 environmentBadge = environmentNode.querySelector(".environment-badge"); - environmentBadge.textContent = environment.failedTests === 0 ? "Passing" : "Issues"; - environmentBadge.className = `environment-badge ${environment.failedTests === 0 ? "passing" : "failing"}`; + const environmentIsPassing = resultsSearchTerm ? visibleFailedTests === 0 : environment.failedTests === 0; + environmentBadge.textContent = environmentIsPassing ? "Passing" : "Issues"; + environmentBadge.className = `environment-badge ${environmentIsPassing ? "passing" : "failing"}`; const endpointList = environmentNode.querySelector(".endpoint-list"); - for (const endpoint of environment.endpoints) { + for (const endpointEntry of endpoints) { + const { endpoint, endpointMatches, tests } = endpointEntry; const endpointKey = getResultEndpointKey(environment, endpoint); const endpointNode = endpointTemplate.content.firstElementChild.cloneNode(true); - endpointNode.open = resultExpansionState.endpoints.get(endpointKey) ?? !endpoint.isSuccess; + endpointNode.open = resultsSearchTerm ? true : resultExpansionState.endpoints.get(endpointKey) ?? !endpoint.isSuccess; endpointNode.addEventListener("toggle", () => { resultExpansionState.endpoints.set(endpointKey, endpointNode.open); }); - endpointNode.querySelector(".endpoint-name").textContent = endpoint.name; - endpointNode.querySelector(".endpoint-meta").textContent = - `${endpoint.method} ${endpoint.requestUrl} - ${Math.round(endpoint.durationMs)} ms`; + endpointNode.querySelector(".endpoint-name").innerHTML = highlightMatch(endpoint.name, resultsSearchTerm); + endpointNode.querySelector(".endpoint-meta").innerHTML = + `${highlightMatch(endpoint.method, resultsSearchTerm)} ${highlightMatch(endpoint.requestUrl, resultsSearchTerm)} - ${Math.round(endpoint.durationMs)} ms`; const endpointBadge = endpointNode.querySelector(".endpoint-badge"); - endpointBadge.textContent = endpoint.isSuccess ? "Pass" : "Fail"; - endpointBadge.className = `endpoint-badge ${endpoint.isSuccess ? "passing" : "failing"}`; + const endpointVisibleFailedTests = tests.filter((test) => !test.isSuccess).length; + const endpointIsPassing = resultsSearchTerm ? endpointVisibleFailedTests === 0 : endpoint.isSuccess; + endpointBadge.textContent = endpointIsPassing ? "Pass" : "Fail"; + endpointBadge.className = `endpoint-badge ${endpointIsPassing ? "passing" : "failing"}`; endpointNode.querySelector(".response-body").textContent = endpoint.responseBody || endpoint.errorMessage || "(empty response)"; const testList = endpointNode.querySelector(".test-list"); - endpoint.tests.forEach((test, testIndex) => { + tests.forEach((test, testIndex) => { const testKey = getResultTestKey(environment, endpoint, test, testIndex); const testNode = testTemplate.content.firstElementChild.cloneNode(true); - testNode.open = resultExpansionState.tests.get(testKey) ?? !test.isSuccess; + testNode.open = resultsSearchTerm ? true : resultExpansionState.tests.get(testKey) ?? !test.isSuccess; testNode.addEventListener("toggle", () => { resultExpansionState.tests.set(testKey, testNode.open); }); - testNode.querySelector(".test-name").textContent = test.name; + testNode.querySelector(".test-name").innerHTML = highlightMatch(test.name, resultsSearchTerm); const testBadge = testNode.querySelector(".test-badge"); testBadge.textContent = test.isSuccess ? "Pass" : "Fail"; @@ -419,7 +445,7 @@ function renderState(state) { const expectedText = `Expected ${test.expectedStatus}, actual ${test.actualStatus ?? "n/a"}`; const errorSuffix = test.errorMessage ? ` - ${test.errorMessage}` : ""; - testNode.querySelector(".test-status-line").textContent = `${expectedText}${errorSuffix}`; + testNode.querySelector(".test-status-line").innerHTML = highlightMatch(`${expectedText}${errorSuffix}`, resultsSearchTerm); const assertionList = testNode.querySelector(".assertion-list"); if (test.assertions.length === 0) { @@ -428,7 +454,7 @@ function renderState(state) { for (const assertion of test.assertions) { const listItem = document.createElement("li"); listItem.className = assertion.isSuccess ? "assertion-pass" : "assertion-fail"; - listItem.textContent = `${assertion.rule} on ${assertion.field}: ${assertion.message}`; + listItem.innerHTML = `${highlightMatch(assertion.rule, resultsSearchTerm)} on ${highlightMatch(assertion.field, resultsSearchTerm)}: ${highlightMatch(assertion.message, resultsSearchTerm)}`; assertionList.appendChild(listItem); } } @@ -443,6 +469,76 @@ function renderState(state) { } } +function filterRunEnvironments(run, searchTerm) { + if (!searchTerm) { + return run.environments.map((environment) => ({ + environment, + environmentMatches: false, + endpoints: environment.endpoints.map((endpoint) => ({ + endpoint, + endpointMatches: false, + tests: endpoint.tests + })) + })); + } + + return run.environments + .map((environment) => { + const environmentMatches = + matchesSearch(environment.name, searchTerm) || + matchesSearch(environment.baseUrl, searchTerm); + + const endpoints = environment.endpoints + .map((endpoint) => { + const endpointMatches = environmentMatches || endpointMatchesRunSearch(endpoint, searchTerm); + return { + endpoint, + endpointMatches, + tests: endpointMatches + ? endpoint.tests + : endpoint.tests.filter((test) => testMatchesRunSearch(test, searchTerm)) + }; + }) + .filter((endpointEntry) => endpointEntry.endpointMatches || endpointEntry.tests.length > 0); + + if (!environmentMatches && endpoints.length === 0) { + return null; + } + + return { + environment, + environmentMatches, + endpoints + }; + }) + .filter((entry) => entry !== null); +} + +function endpointMatchesRunSearch(endpoint, searchTerm) { + return ( + matchesSearch(endpoint.name, searchTerm) || + matchesSearch(endpoint.method, searchTerm) || + matchesSearch(endpoint.requestUrl, searchTerm) || + matchesSearch(endpoint.errorMessage, searchTerm) + ); +} + +function testMatchesRunSearch(test, searchTerm) { + return ( + matchesSearch(test.name, searchTerm) || + matchesSearch(test.errorMessage, searchTerm) || + test.assertions.some((assertion) => assertionMatchesSearch(assertion, searchTerm)) + ); +} + +function assertionMatchesSearch(assertion, searchTerm) { + return ( + matchesSearch(assertion.field, searchTerm) || + matchesSearch(assertion.rule, searchTerm) || + matchesSearch(assertion.message, searchTerm) + ); +} + function synchronizeResultExpansionState(run) { const environmentKeys = new Set(); const endpointKeys = new Set(); @@ -611,8 +707,26 @@ function setResultExpansion(isOpen) { renderState(lastRunState); } +function matchesSearch(value, searchTerm) { + return typeof value === "string" && value.toLowerCase().includes(searchTerm); +} + +function highlightMatch(value, searchTerm) { + const safeValue = escapeHtml(value); + if (!searchTerm || typeof value !== "string") { + return safeValue; + } + + const pattern = new RegExp(`(${escapeRegExp(searchTerm)})`, "ig"); + return safeValue.replace(pattern, "$1"); +} + +function escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + function escapeHtml(value) { - return value + return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">"); @@ -652,5 +766,12 @@ selectionSearchInput.addEventListener("input", () => { renderSelection(suiteManifest); } }); +resultsSearchInput.addEventListener("input", () => { + resultsSearchTerm = resultsSearchInput.value.trim().toLowerCase(); + + if (lastRunState) { + renderState(lastRunState); + } +}); initializeDashboard(); diff --git a/src/ApiTestRunner.App/wwwroot/index.html b/src/ApiTestRunner.App/wwwroot/index.html index b32203a..88414ed 100644 --- a/src/ApiTestRunner.App/wwwroot/index.html +++ b/src/ApiTestRunner.App/wwwroot/index.html @@ -189,6 +189,14 @@

+
+
+ +
+
diff --git a/src/ApiTestRunner.App/wwwroot/styles.css b/src/ApiTestRunner.App/wwwroot/styles.css index 2e821a5..594b6f1 100644 --- a/src/ApiTestRunner.App/wwwroot/styles.css +++ b/src/ApiTestRunner.App/wwwroot/styles.css @@ -260,6 +260,13 @@ body.app-shell { color: #94a3b8; } +.search-highlight { + padding: 0 0.14rem; + border-radius: 0.3rem; + background: rgba(255, 193, 7, 0.38); + color: inherit; +} + .primary-button, .ghost-button { display: inline-flex; 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 debdb93f8f3fa69b6e000117ed7ff9e74b47d229..18ecb7a9f429514870f93e18ce5f85f1a892819b 100644 GIT binary patch delta 252 zcmZp8!r1VHaY6^n!3{6xZ|qqUDezBf!E7!Kd#|?Z>qP^W-@LWiBWfjwUXq!qv0
    LkKAU_XH9(+w+1-8820NWrL?u?sFH%yTtdQ2E026!x6$Gg+xvBL$!TO!eH>M8z z&4<#yvG^x3m@$|#7&90$qyotlATNa>iNOfWGXlyOFeEcrg4LukSO9ru3~4~p0w|gS WRG$XK7C==XJtjb2(q{IYpR53KT2Iyh delta 252 zcmZp8!r1VHaY6^nnN;f2$iR@z$dER9LbBv!*;KL3e#v=E zERQbMEt-5FH9+8S_@xa!efRz6-})WS9eJg5vO-#u0#vX+9x4b_tz`3HhQM>12?y@% z?>@HqP}(;Ze{%+Nh9m}KhBSs0AZY;PnKGC$qyotlAT|V&hCopRhD0DVV6X&|7C`lh ZKo&@CG7zSK^&0?Lh75_D*>irf0s!ddRx1Di 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 7631004114f77400b65fe4e3f5a52eabac35b302..30080e0c0ea5ae77d0278c1a89e5d47edec04a72 100644 GIT binary patch delta 1068 zcmca}l=03{#tAhd&CBlYlQ!7tv?408T7Hp|vV~{mvxyVVS$TvqAOR+Z*I`TyF5yfJ zc|hC;#OuPD8McNaDPdq{V2)sB5Q&()kgfi65>6-3i^Uz0K<&*EBgU*QK^o4t?7o<4si<| z#Xgi=2$HUGlXsg`N78(*VFl9{QoVXAS8SxS;osv+$bWtxku2`N5!A<07GaP zh$sXR)gYoBL`(+}D?r5l$v_D0Wh&jRh^OGIfRQ;{k z)PeFZfFv7(8Rs;hJqH-K0m<7;*MQ_U=5Ijq4vPW@J3~>V0tW{}L!<*IJHvAz$-(e7 zvH{5Ui#h-#JEIu5*clFP-pVG-RzKm1f3T0_r=9A2yL$vSzZW@mAb8tb-#-T>KOLB9 zyPQ{~F-C%gfnn`r-|WK%JZxjbp-7&MAV|EQNR^-+pk8WY18MnR0D{V8(07 zkb4&@+~e<^YF>QoPX_1bAJgaUmh;^oUYW{u zzAskVW-K+?JK^*58G%z;Zp(enD0+P=*?h(S2eC^t&CWfw@KnEUGI8=m8CA|CqocEV zmV0WooH^Dhkyw2@Jv~)*$K{K8qS>#e{P;f2xcLKL0h7cXjXjGP9xyO8wz8b9gf+5kG)L7z>Dy1`(zpA`nEBO#U9HW=y16 Z$RJ>69uXd+~($4_M(Tm$ktcW zXnPJf;|*qia|UyUBnD%KG=>x)X#nJzGMF)>0?8C0HUyG}Kv4sRL?AR^umqA8K=p}0 Z7D#O}5T=0j8vt2`42jz(^E2i$0RRsYP9XpQ 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 7a0e0c3aeed9ec28ad515d08477cd3c0f48d0bd8..a297b3709b484ac9110c6888c3a447bc4b107566 100644 GIT binary patch delta 2446 zcma*nc~DbV6bA5C-3G>Ce>xa z>M~(Lrj6b)*7|;0$mGpaV#4A@b1pY1+^P>1SPR1M%%%@65v7HR=n6DJ?i>+SLK}3> z5tA@nOj>w5S4^S^F*(CRD1~wu7%8UDAS}`#5ZUQ0$T!M@Dxn9K&9kIJ&_r9(57Aas z3A5*0Q4D0l4yc4DU=w3SQDA_bPzP;bAB(+E8Y`m%vFzM3niMCaP}l_>;1@5WpP)pC z*bN8cg>(!~!v&~?2Dk-xL6#sS8*l&@@B{^ng#ZYGUxhqF6QFy3(7rfOR`N0=31!lr*HQP9n zqev7g)5fKkuY@efg#y?J#ZbmK1ank@;)toC2J?Dof;;dKI-nEYfhdt9ORxrekb^sn z0^dZ_QTGv7w`lHjD-LQf?y3hnU14S6fqhBC2QE}*d?M@8WHt?WPynKfh`OH_XWM{ z-k6VtFHQ9{e&m}?A)>EMYvI@zW16REd;81z5p#ownWNzO{U54H9zXWEMA6O3vE2q2@F}TY|X(X-?aJxt)ry$xpA9Iy@jr)yXc$cU4?cx$Jn;LbX|P#l;qLnQ)~W`c0Dkh^a^ka z`Yy^0FKM4))=X}6|L;2jd(K@oxZF-X(LOu%)5n2T+49tD*~_ahsvWA`hHZ-I{PT}+ zPp{P>zZgQ@I{Z5n!F7k1)jix=<{2E}dZYa6_60f5)3+>C9j_DQ7^Dt^;@oxALR{Kq z*L_Rfz30}1+vy!c=IuE=X?G--Ri!W4x45NL-g+#dqiIE4`0rC4ILEWcCYD^!pE~00 zi$;Ned(F`w71MPOyBkU$ZLKOv73aD2Z@hOd#b;V#NI`mf^NiR?X~dHH=1g1H`U6Mn zuBT|Wcy74iZ(Y1>S4!piiT`N5Uh8%&bvj|YXj*+m-JBN{we6!HXbaZp^qQtt|Nh=I<8-F@KSfiEt&H4vr#KHEZ}O?&fWcu@h!GDg7!~+7x;2opk}zk{lZw!)hC}C zmibsrxE8W;vsLlBmtDa}^D}e{}MK=efx}IDI9!erH`*dHQvuxGfv^BQH)cml^W$ZUv@t&*<%8r;Dy<-R#RnKR2GwIUyX=hjxB{m^qqa tt5~!cjcY3e@{e^Ib-A5_VISMYf2whF-cDg(-j-$!_G|VVlk=lR{{o&Khb;gA delta 2449 zcma*nc~DbV6bA5<9r#MGc!WR6!); zI&Mf|3L`F!OD$D9gCdAjL23nQU2&&aOL3-(^t+Et$NnXm`F;1Eckju&dGjW{Vji(B&1QGYb=%WuuN#Ptdz#Yh4$WawMgsz1~ zL=i@$hJPZBNGLa=VUPgDP!86MjK~4P7U=|>tyV%lizQSAJ&?S_j0z!EVMav?bE<;* zOU+3EX>b&(;5k?=GbcIhgm2&`JOrC4?1kbeDOEY)i*;69ke2+0cUKnAYh34Smcf?zg;f*hhi4T-Q0(jg1-pb(1TThN|B zoP`T;8S0@4THroBg+JhLcn>0#kR(}sp?m^Z>5xCc+51G?ZH2;)R#3YK64L%|uw zfKQy^s1s3q9@lqN4dzL(0XD-H*v@ZUDIy(;gNBXen4g4ma1pM+b+`qs@Bp4cCv-y( zaB2~mffd++3|zqz{9rN!!E7}mj)u`iBOw-MZud;q~3e!kw8DC3DgE_O>AQfXO>yl}ul=)v1Yn0M< zEb3rC6hry{*6~Uv;FW|LXnb#074399MVOv!$jid}z)tZc^7xGp%d|Wmf(Y z=-$z;$t#mttjxT7O`*w>I9STlg>gB_1zgZf=Zo$yDL?06ll?eh;*{)b>GpfNo>xwu z@Sk!G4Y%#Yx#ox^ zz71I|<;T<$9X!q^1RaVz9cqFV@dH*4xWVsq_j^PKZ$kc=@YTu`xbgDsQF!@Yvotv%-1> zKhXkX7EUbOS@^T~f`y#LS1htv>|s&D;xvnT7VU^|vHnT=Rl(T)d>j8oeeL?TP41jq zf13CkpVoV4rSt;Vr@mgP#Qzi86M?1eiT#Tlu vY8Gw!Gr7kEvQKsDGxNFy&Y#-Ff2;n$wlZOVo|ZZr`_;$vD+?4H{+s>>JM0Fw delta 16 XcmZ1#yefD?DQm_h%ZjFrRR)>>KII1X 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 debdb93f8f3fa69b6e000117ed7ff9e74b47d229..18ecb7a9f429514870f93e18ce5f85f1a892819b 100644 GIT binary patch delta 252 zcmZp8!r1VHaY6^n!3{6xZ|qqUDezBf!E7!Kd#|?Z>qP^W-@LWiBWfjwUXq!qv0
      LkKAU_XH9(+w+1-8820NWrL?u?sFH%yTtdQ2E026!x6$Gg+xvBL$!TO!eH>M8z z&4<#yvG^x3m@$|#7&90$qyotlATNa>iNOfWGXlyOFeEcrg4LukSO9ru3~4~p0w|gS WRG$XK7C==XJtjb2(q{IYpR53KT2Iyh delta 252 zcmZp8!r1VHaY6^nnN;f2$iR@z$dER9LbBv!*;KL3e#v=E zERQbMEt-5FH9+8S_@xa!efRz6-})WS9eJg5vO-#u0#vX+9x4b_tz`3HhQM>12?y@% z?>@HqP}(;Ze{%+Nh9m}KhBSs0AZY;PnKGC$qyotlAT|V&hCopRhD0DVV6X&|7C`lh ZKo&@CG7zSK^&0?Lh75_D*>irf0s!ddRx1Di 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 7631004114f77400b65fe4e3f5a52eabac35b302..30080e0c0ea5ae77d0278c1a89e5d47edec04a72 100644 GIT binary patch delta 1068 zcmca}l=03{#tAhd&CBlYlQ!7tv?408T7Hp|vV~{mvxyVVS$TvqAOR+Z*I`TyF5yfJ zc|hC;#OuPD8McNaDPdq{V2)sB5Q&()kgfi65>6-3i^Uz0K<&*EBgU*QK^o4t?7o<4si<| z#Xgi=2$HUGlXsg`N78(*VFl9{QoVXAS8SxS;osv+$bWtxku2`N5!A<07GaP zh$sXR)gYoBL`(+}D?r5l$v_D0Wh&jRh^OGIfRQ;{k z)PeFZfFv7(8Rs;hJqH-K0m<7;*MQ_U=5Ijq4vPW@J3~>V0tW{}L!<*IJHvAz$-(e7 zvH{5Ui#h-#JEIu5*clFP-pVG-RzKm1f3T0_r=9A2yL$vSzZW@mAb8tb-#-T>KOLB9 zyPQ{~F-C%gfnn`r-|WK%JZxjbp-7&MAV|EQNR^-+pk8WY18MnR0D{V8(07 zkb4&@+~e<^YF>QoPX_1bAJgaUmh;^oUYW{u zzAskVW-K+?JK^*58G%z;Zp(enD0+P=*?h(S2eC^t&CWfw@KnEUGI8=m8CA|CqocEV zmV0WooH^Dhkyw2@Jv~)*$K{K8qS>#e{P;f2xcLKL0h7cXjXjGP9xyO8wz8b9gf+5kG)L7z>Dy1`(zpA`nEBO#U9HW=y16 Z$R|*}g6xOk>nKj96vmncR#?Aj(YZWo2dekQ{ z=_Q$&8XKk>rK`}!W9uus*&n*~|kGj9IRTC0dD)uTRv zNzdFo$v7>=*udP>EY&o{)G)=+z#uWvz%tPyDbdovJUKZf$=D#-Fp+^Fn~@=Hvc9(D z=47o+_6d-8;?6d*PPl7>K01BOH(G+?j|*}g6xOk>nKj96vmncR#?Aj(YZWo2dekQ{ z=_Q$&8XKk>rK`}!W9uus*&n*~|kGj9IRTC0dD)uTRv zNzdFo$v7>=*udP>EY&o{)G)=+z#uWvz%tPyDbdovJUKZf$=D#-Fp+^Fn~@=Hvc9(D z=47o+_6d-8;?6d*PPl7>K01BOH(G+?j