From 008d0e479ebbf1d1175c0a0585814cd6506770af Mon Sep 17 00:00:00 2001 From: Svetoslav Krastev Date: Mon, 13 Jan 2025 17:15:24 +0200 Subject: [PATCH] Add generic sample tests and test generation. (#823) --- .gitignore | 2 + azure-pipelines/NuGet.config | 7 + .../templates/build-steps-template.yml | 4 +- browser/.runsettings | 18 ++ browser/IgBlazorSamples.Gulp/gulpfile.js | 18 ++ .../tasks/gulp-samples.js | 19 +- browser/IgBlazorSamples.Test.sln | 54 +++++ browser/IgBlazorSamples.Test/BasicTests.cs | 214 ++++++++++++++++++ .../IgBlazorSamples.Test.csproj | 46 ++++ .../Infrastructure/BlazorTest.cs | 108 +++++++++ .../Models/SettingsModel.cs | 45 ++++ browser/IgBlazorSamples.Test/ReadMe.md | 94 ++++++++ .../IgBlazorSamples.Test/SampleErrorTests.cs | 56 +++++ browser/IgBlazorSamples.Test/TestUtils.cs | 87 +++++++ .../IgBlazorSamples.Test/testsettings.json | 210 +++++++++++++++++ .../IgBlazorSamples.TestServer.csproj | 23 ++ .../Pages/Error.cshtml | 42 ++++ .../Pages/Error.cshtml.cs | 26 +++ browser/IgBlazorSamples.TestServer/Program.cs | 35 +++ .../Properties/AssemblyInfo.cs | 3 + .../Properties/launchSettings.json | 29 +++ browser/IgBlazorSamples.TestServer/ReadMe.md | 2 + .../appsettings.Development.json | 8 + .../appsettings.json | 9 + 24 files changed, 1153 insertions(+), 6 deletions(-) create mode 100644 azure-pipelines/NuGet.config create mode 100644 browser/.runsettings create mode 100644 browser/IgBlazorSamples.Test.sln create mode 100644 browser/IgBlazorSamples.Test/BasicTests.cs create mode 100644 browser/IgBlazorSamples.Test/IgBlazorSamples.Test.csproj create mode 100644 browser/IgBlazorSamples.Test/Infrastructure/BlazorTest.cs create mode 100644 browser/IgBlazorSamples.Test/Models/SettingsModel.cs create mode 100644 browser/IgBlazorSamples.Test/ReadMe.md create mode 100644 browser/IgBlazorSamples.Test/SampleErrorTests.cs create mode 100644 browser/IgBlazorSamples.Test/TestUtils.cs create mode 100644 browser/IgBlazorSamples.Test/testsettings.json create mode 100644 browser/IgBlazorSamples.TestServer/IgBlazorSamples.TestServer.csproj create mode 100644 browser/IgBlazorSamples.TestServer/Pages/Error.cshtml create mode 100644 browser/IgBlazorSamples.TestServer/Pages/Error.cshtml.cs create mode 100644 browser/IgBlazorSamples.TestServer/Program.cs create mode 100644 browser/IgBlazorSamples.TestServer/Properties/AssemblyInfo.cs create mode 100644 browser/IgBlazorSamples.TestServer/Properties/launchSettings.json create mode 100644 browser/IgBlazorSamples.TestServer/ReadMe.md create mode 100644 browser/IgBlazorSamples.TestServer/appsettings.Development.json create mode 100644 browser/IgBlazorSamples.TestServer/appsettings.json diff --git a/.gitignore b/.gitignore index 503f30c60f..55126a438c 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,8 @@ /browser/Tests +/browser/IgBlazorSamples.Test/Generated/** + # misc .DS_Store .env.local diff --git a/azure-pipelines/NuGet.config b/azure-pipelines/NuGet.config new file mode 100644 index 0000000000..d1d310ef48 --- /dev/null +++ b/azure-pipelines/NuGet.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/azure-pipelines/templates/build-steps-template.yml b/azure-pipelines/templates/build-steps-template.yml index 7605740ab4..3b92b025ce 100644 --- a/azure-pipelines/templates/build-steps-template.yml +++ b/azure-pipelines/templates/build-steps-template.yml @@ -24,9 +24,9 @@ steps: customCommand: ci - task: CmdLine@2 - displayName: 'npx gulp copySamplesTo${{ parameters.projectToBuild }}' + displayName: 'npx gulp copySamplesTo${{ parameters.projectToBuild }}CI' inputs: - script: 'npx gulp copySamplesTo${{ parameters.projectToBuild }}' + script: 'npx gulp copySamplesTo${{ parameters.projectToBuild }}CI' workingDirectory: '$(Build.SourcesDirectory)\browser\IgBlazorSamples.Gulp' failOnStderr: true diff --git a/browser/.runsettings b/browser/.runsettings new file mode 100644 index 0000000000..0f5ac21d8b --- /dev/null +++ b/browser/.runsettings @@ -0,0 +1,18 @@ + + + + 7 + + + chromium + 5000 + + + + + + + + + + \ No newline at end of file diff --git a/browser/IgBlazorSamples.Gulp/gulpfile.js b/browser/IgBlazorSamples.Gulp/gulpfile.js index bb1713866e..0a69459910 100644 --- a/browser/IgBlazorSamples.Gulp/gulpfile.js +++ b/browser/IgBlazorSamples.Gulp/gulpfile.js @@ -66,12 +66,21 @@ exports.updateIG = updateIG = gulp.series( sb.updateIG, ); +// Copy samples task used only for local run of the Samples Browser Server. exports.copySamplesToServer = copySamplesToServer = gulp.series( // sb.updateVersion, sb.getSamples, sb.copySamplesToServer, ); +// Copy samples task used on host agents. Samples Browser Server from there has added '/blazor-samples' to the base url. +exports.copySamplesToServerCI = copySamplesToServerCI = gulp.series( + // sb.updateVersion, + sb.getSamples, + sb.copySamplesToServerCI, +); + +// Copy samples task used only for local run of the SB Client. exports.copySamplesToClient = copySamplesToClient = gulp.series( // sb.updateVersion, sb.getSamples, @@ -81,6 +90,15 @@ exports.copySamplesToClient = copySamplesToClient = gulp.series( ); exports.updateBrowser = updateBrowser = copySamplesToClient; +// Copy samples task used on host agents. Samples Browser Client from there has added '/blazor-samples' to the base url. +exports.copySamplesToClientCI = copySamplesToClientCI = gulp.series( + // sb.updateVersion, + sb.getSamples, + sb.copySamplesToClientCI, + sb.updateCodeViewer, + sb.updateReadme, +); + exports.cleanupSampleBrowsers = cleanupSampleBrowsers = gulp.series( sb.cleanupSampleBrowsers, ); diff --git a/browser/IgBlazorSamples.Gulp/tasks/gulp-samples.js b/browser/IgBlazorSamples.Gulp/tasks/gulp-samples.js index d714e000ad..0f57905a47 100644 --- a/browser/IgBlazorSamples.Gulp/tasks/gulp-samples.js +++ b/browser/IgBlazorSamples.Gulp/tasks/gulp-samples.js @@ -376,7 +376,7 @@ function copySamplePages(cb, outputPath) { cb(); } -function copySampleScripts(cb, outputPath, indexName) { +function copySampleScripts(cb, outputPath, indexName, isLocalBuild) { var insertScriptFiles = []; log('deleting scripts in: ' + outputPath + '/wwwroot/sb/*.js'); @@ -467,7 +467,6 @@ function copySampleScripts(cb, outputPath, indexName) { // indexLines = indexLines.filter((v, i, a) => a.indexOf(v) === i); - var isLocalBuild = __dirname.indexOf('Agent') < 0; for (let i = 0; i < indexLines.length; i++) { if (indexLines[i].indexOf(' 0) { if (isLocalBuild) { @@ -495,19 +494,31 @@ function copySampleScripts(cb, outputPath, indexName) { // '../../browser/IgBlazorSamples.Server/wwwroot' function copySamplesToServer(cb) { cleanupSampleBrowser( "../../browser/IgBlazorSamples.Server"); - copySampleScripts(cb, "../../browser/IgBlazorSamples.Server", "/Pages/_Host.cshtml"); + copySampleScripts(cb, "../../browser/IgBlazorSamples.Server", "/Pages/_Host.cshtml", true); copySamplePages(cb, "../../browser/IgBlazorSamples.Server"); } exports.copySamplesToServer = copySamplesToServer; +function copySamplesToServerCI(cb) { + cleanupSampleBrowser( "../../browser/IgBlazorSamples.Server"); + copySampleScripts(cb, "../../browser/IgBlazorSamples.Server", "/Pages/_Host.cshtml", false); + copySamplePages(cb, "../../browser/IgBlazorSamples.Server"); +} exports.copySamplesToServerCI = copySamplesToServerCI; + // '../../browser/IgBlazorSamples.Client/Pages' // '../../browser/IgBlazorSamples.Client/Services' // '../../browser/IgBlazorSamples.Client/wwwroot' function copySamplesToClient(cb) { cleanupSampleBrowser( "../../browser/IgBlazorSamples.Client"); - copySampleScripts(cb, "../../browser/IgBlazorSamples.Client", "/wwwroot/index.html"); + copySampleScripts(cb, "../../browser/IgBlazorSamples.Client", "/wwwroot/index.html", true); copySamplePages(cb, "../../browser/IgBlazorSamples.Client"); } exports.copySamplesToClient = copySamplesToClient; +function copySamplesToClientCI(cb) { + cleanupSampleBrowser( "../../browser/IgBlazorSamples.Client"); + copySampleScripts(cb, "../../browser/IgBlazorSamples.Client", "/wwwroot/index.html", false); + copySamplePages(cb, "../../browser/IgBlazorSamples.Client"); +} exports.copySamplesToClientCI = copySamplesToClientCI; + function updateReadme(cb) { var changeFilesCount = 0; diff --git a/browser/IgBlazorSamples.Test.sln b/browser/IgBlazorSamples.Test.sln new file mode 100644 index 0000000000..1300dceba7 --- /dev/null +++ b/browser/IgBlazorSamples.Test.sln @@ -0,0 +1,54 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32210.238 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IgBlazorSamples.Client", "IgBlazorSamples.Client\IgBlazorSamples.Client.csproj", "{8D8DE753-4CD1-4B21-9C9F-2E40AE5EED1B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IgBlazorSamples.Shared", "IgBlazorSamples.Shared\IgBlazorSamples.Shared.csproj", "{FBA48919-F510-4D99-A3F6-6743A9C304D2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IgBlazorSamples.Core", "IgBlazorSamples.Core\IgBlazorSamples.Core.csproj", "{B64DD2DE-32F6-4CF8-B9F5-7F0E29796460}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F85EE908-E593-4D66-A46D-2193EF3C98C2}" + ProjectSection(SolutionItems) = preProject + .runsettings = .runsettings + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IgBlazorSamples.TestServer", "IgBlazorSamples.TestServer\IgBlazorSamples.TestServer.csproj", "{3E6A34E9-442D-4CC0-BE26-0C872B51FF55}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IgBlazorSamples.Test", "IgBlazorSamples.Test\IgBlazorSamples.Test.csproj", "{EFFA7E4D-7130-40AE-9E8B-6110FCB099AF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8D8DE753-4CD1-4B21-9C9F-2E40AE5EED1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D8DE753-4CD1-4B21-9C9F-2E40AE5EED1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D8DE753-4CD1-4B21-9C9F-2E40AE5EED1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D8DE753-4CD1-4B21-9C9F-2E40AE5EED1B}.Release|Any CPU.Build.0 = Release|Any CPU + {FBA48919-F510-4D99-A3F6-6743A9C304D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBA48919-F510-4D99-A3F6-6743A9C304D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBA48919-F510-4D99-A3F6-6743A9C304D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBA48919-F510-4D99-A3F6-6743A9C304D2}.Release|Any CPU.Build.0 = Release|Any CPU + {B64DD2DE-32F6-4CF8-B9F5-7F0E29796460}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B64DD2DE-32F6-4CF8-B9F5-7F0E29796460}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B64DD2DE-32F6-4CF8-B9F5-7F0E29796460}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B64DD2DE-32F6-4CF8-B9F5-7F0E29796460}.Release|Any CPU.Build.0 = Release|Any CPU + {3E6A34E9-442D-4CC0-BE26-0C872B51FF55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E6A34E9-442D-4CC0-BE26-0C872B51FF55}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E6A34E9-442D-4CC0-BE26-0C872B51FF55}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E6A34E9-442D-4CC0-BE26-0C872B51FF55}.Release|Any CPU.Build.0 = Release|Any CPU + {EFFA7E4D-7130-40AE-9E8B-6110FCB099AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFFA7E4D-7130-40AE-9E8B-6110FCB099AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFFA7E4D-7130-40AE-9E8B-6110FCB099AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFFA7E4D-7130-40AE-9E8B-6110FCB099AF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2B229BA4-53C2-4C4E-8A25-BA19866A265A} + EndGlobalSection +EndGlobal diff --git a/browser/IgBlazorSamples.Test/BasicTests.cs b/browser/IgBlazorSamples.Test/BasicTests.cs new file mode 100644 index 0000000000..3a1341ca85 --- /dev/null +++ b/browser/IgBlazorSamples.Test/BasicTests.cs @@ -0,0 +1,214 @@ +using IgBlazorSamples.Test; +using Microsoft.Playwright; +using System.Text; + +namespace PlaywrightTests; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class PreflightTests : BlazorTest +{ + [Test] + [Category("PreflightTest")] + public async Task PreflightTest() + { + int numPageErrors = 0; + StringBuilder messages = new(); + Page.Console += (_, value) => + { + if (value.Type == "warning" && value.Text.Contains("Error:")) + { + messages.AppendLine(value.Text); + numPageErrors++; + } + }; + + await Page.GotoAsync(RootUri.AbsoluteUri); + + await Page.WaitForSelectorAsync("text=This web application demonstrates samples for Infragistics Blazor Controls running on client-side"); + + Assert.That(numPageErrors == 0, $"The following errors were thrown: \n ${messages}"); + } +} + + +class GridLinks +{ + public static string[] FixtureArgs = { + "/samples/grids/grid/action-strip", + "/samples/grids/grid/advanced-filtering-options", + "/samples/grids/grid/advanced-filtering-style", + "/samples/grids/grid/binding-composite-data" + }; +} + +[Parallelizable(ParallelScope.Self)] +[TestFixtureSource(typeof(GridLinks), nameof(GridLinks.FixtureArgs))] +public class ParamTests : BlazorTest +{ + private string goToUrl; + public ParamTests(string goToUrl) + { + this.goToUrl = goToUrl; + } + + [SetUp] + public void InitFixture() + { + Context.SetDefaultTimeout(TestContext.Parameters.Get("defaultTimeout", 30000)); + } + + [Test] + public async Task BasicParamTest() + { + int numPageErrors = 0; + StringBuilder messages = new(); + Page.Console += (_, value) => + { + if (value.Type == "warning" && value.Text.Contains("Error:")) + { + messages.AppendLine(value.Text); + numPageErrors++; + } + }; + + await Page.GotoAsync(String.Format("{0}/{1}", RootUri.AbsoluteUri.TrimEnd('/'), goToUrl.TrimStart('/'))); + + await Page.WaitForSelectorAsync("igx-grid-header"); + + Assert.That(numPageErrors == 0, $"The following errors were thrown: \n ${messages}"); + } +} + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class BasicError : BlazorTest +{ + [Test] + public async Task BasicErrorTest() + { + int numPageErrors = 0; + StringBuilder messages = new(); + Page.Console += (_, value) => + { + if (value.Type == "warning" && value.Text.Contains("Error:")) + { + messages.AppendLine(value.Text); + numPageErrors++; + } + }; + + await Page.GotoAsync($"{RootUri.AbsoluteUri}samples/grids/grid/clipboard-operations"); + + await Page.WaitForSelectorAsync("igx-grid-header"); + + Assert.That(numPageErrors == 0, $"The following errors were thrown: \n ${messages}"); + } +} + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class BasicPin : BlazorTest +{ + [Test] + public async Task BasicPinTest() + { + await Page.GotoAsync($"{RootUri.AbsoluteUri}samples/grids/grid/column-pinning"); + + await Page.WaitForSelectorAsync("igx-grid-header"); + + await Page.GetByRole(AriaRole.Button, new() { Name = "2" }).ClickAsync(); + await Page.Locator("#igx-checkbox-1").GetByRole(AriaRole.Img).ClickAsync(); + await Page.Locator("#igx-checkbox-3").GetByRole(AriaRole.Img).ClickAsync(); + + await Expect(Page.Locator("igx-grid-header").Nth(1)).ToContainTextAsync("Company Name"); + } +} + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class BasicResize : BlazorTest +{ + [Test] + public async Task BasicResizeTest() + { + await Page.GotoAsync($"{RootUri.AbsoluteUri}samples/grids/grid/column-resizing"); + + await Page.WaitForSelectorAsync("igx-grid-header"); + + var companyBoundBox = await Page.GetByRole(AriaRole.Columnheader, new() { Name = "Company" }).BoundingBoxAsync(); + Assert.That(companyBoundBox?.Width, Is.EqualTo(136)); + + var companyResizer = Page.Locator("#grid_-1_0_1 span").Nth(2); + var companyResizerBoundBox = await companyResizer.BoundingBoxAsync(); + + await companyResizer.HoverAsync(); + + await Page.Mouse.DownAsync(); + await Page.WaitForTimeoutAsync(100); + + Assert.That(companyResizerBoundBox, Is.Not.Null); + await Page.Mouse.MoveAsync(companyResizerBoundBox.X - 50, 0); + await Page.WaitForTimeoutAsync(100); + + await Page.Mouse.UpAsync(); + await Page.WaitForTimeoutAsync(100); + + companyBoundBox = await Page.GetByRole(AriaRole.Columnheader, new() { Name = "Company" }).BoundingBoxAsync(); + Assert.That(companyBoundBox?.Width, Is.EqualTo(84)); + } +} + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class BasicScroll : BlazorTest +{ + [Test, Timeout(30000)] + public async Task BasicScrollTest() + { + await Page.GotoAsync($"{RootUri.AbsoluteUri}samples/grids/grid/groupby-summary-options"); + + await Page.WaitForSelectorAsync("igx-grid-header"); + + await Page.Locator("#input-2").ClickAsync(); + await Page.GetByText("ChildLevelsOnly").ClickAsync(); + + await Page.Locator("#input-3").ClickAsync(); + await Page.GetByText("Bottom").ClickAsync(); + + // Method 1 - Mouse wheel manually to bottom. Slower but works on headless so far. + await Page.Locator(".igx-grid__tbody-content").HoverAsync(); + + while (!(await Page.Locator("igx-grid-summary-row").IsVisibleAsync())) + { + await Page.Mouse.WheelAsync(0, 100); + } + + // Method 2 - Hold down mouse button at the end of scrollbar till visible. + // Much faster than method 1. Doesn't work in headless atm. + //var scrollbar = Page.Locator("igx-virtual-helper"); + //var scrollbarBoundBox = await scrollbar.BoundingBoxAsync(); + + //await scrollbar.HoverAsync(new() + //{ + // Position = new Position() + // { + // X = 5, + // Y = (scrollbarBoundBox.Height - 20) + // } + //}); + + //await Page.Mouse.DownAsync(new() { }); + + ////Scroll until the group summary row is rendered + //await Page.WaitForSelectorAsync("igx-grid-summary-row", new() { Timeout = 5000 }); + ////Or until the last row number is rendered + ////await Page.WaitForSelectorAsync("//*[@role='row'][@data-rowindex='500']", new() { Timeout = 5000 }); + + //await Page.Mouse.UpAsync(); + //await Page.WaitForTimeoutAsync(100); + + await Expect(Page.Locator("#igx-grid-0_500_2")).ToContainTextAsync("Count 499 Min $5,030.24 Max $29,980.24 Sum $8,838,532.26 Avg $17,712.49"); + } + +} \ No newline at end of file diff --git a/browser/IgBlazorSamples.Test/IgBlazorSamples.Test.csproj b/browser/IgBlazorSamples.Test/IgBlazorSamples.Test.csproj new file mode 100644 index 0000000000..8c063e08d7 --- /dev/null +++ b/browser/IgBlazorSamples.Test/IgBlazorSamples.Test.csproj @@ -0,0 +1,46 @@ + + + + net8.0 + enable + enable + + false + true + + + + + Always + + + Always + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/browser/IgBlazorSamples.Test/Infrastructure/BlazorTest.cs b/browser/IgBlazorSamples.Test/Infrastructure/BlazorTest.cs new file mode 100644 index 0000000000..e6fd892f89 --- /dev/null +++ b/browser/IgBlazorSamples.Test/Infrastructure/BlazorTest.cs @@ -0,0 +1,108 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Playwright.NUnit; + +namespace IgBlazorSamples.Test; + +/// +/// Base Playwright PageTest setup with in-memory Test Server application +/// +public class BlazorTest : PageTest +{ + /// + /// Root Uri that the app can be accessed through and is being intercepted for network requests. + /// Each test connects to it to access the samples browser. + /// + protected Uri RootUri = new("http://127.0.0.1"); + + /// + /// Factory to create in-memory client for the samples test application. + /// + /// We use the Program.cs from IgBlazorSamples.TestServer since the factory creates only servers and IgBlazorSamples.Client is only WASM. + /// + /// + private readonly WebApplicationFactory _webAppFactory = new(); + + /// + /// Test Server in-memory client + /// + private HttpClient? _httpClient; + + [SetUp] + public async Task BlazorSetup() + { + bool createInMemory = TestContext.Parameters.Get("useInMemoryClient", true); + if (createInMemory) + { + _httpClient = _webAppFactory.CreateClient(new() + { + BaseAddress = RootUri, + }); + + await Context.RouteAsync($"{RootUri.AbsoluteUri}**", async route => + { + // Get the request sent from Playwright + var request = route.Request; + var content = request.PostDataBuffer is { } postDataBuffer + ? new ByteArrayContent(postDataBuffer) + : null; + + // Convert to HTTP request for the in-memory Test Server client. + var requestMessage = new HttpRequestMessage(new(request.Method), request.Url) + { + Content = content, + }; + foreach (var header in request.Headers) + { + requestMessage.Headers.Add(header.Key, header.Value); + } + + var response = await _httpClient.SendAsync(requestMessage); + + // Use response from the Test Server and return it back to Playwright. We intercept all so no need to check for other cases. + var responseBody = await response.Content.ReadAsByteArrayAsync(); + var responseHeaders = response.Content.Headers.Select(h => KeyValuePair.Create(h.Key, string.Join(",", h.Value))); + await route.FulfillAsync(new() + { + BodyBytes = responseBody, + Headers = responseHeaders, + Status = (int)response.StatusCode, + }); + }); + } + else + { + RootUri = new(TestContext.Parameters.Get("testUrl", "localhost:4200")); + } + + // Prepare each test for tracing, in case it fails. + // Required for each test, since we don't know if it's gonna fail or not. + await Context.Tracing.StartAsync(new() + { + Title = $"{TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.Name}", + Screenshots = true, + Snapshots = true, + Sources = true + }); + } + + [TearDown] + public async Task BlazorTearDown() + { + _httpClient?.Dispose(); + + // Check if test failed based on the outcome. + var failed = TestContext.CurrentContext.Result.Outcome == NUnit.Framework.Interfaces.ResultState.Error + || TestContext.CurrentContext.Result.Outcome == NUnit.Framework.Interfaces.ResultState.Failure; + + // Finish the test tracing and set the save path of the trace if it failed. + // Otherwise do not provide path and the trace will not be saved. + await Context.Tracing.StopAsync(new() + { + Path = failed ? Path.Combine( + TestContext.CurrentContext.WorkDirectory, + "playwright-traces", + $"{Environment.MachineName}.{TestContext.CurrentContext.Test.Name}.zip" + ) : null, + }); + } +} diff --git a/browser/IgBlazorSamples.Test/Models/SettingsModel.cs b/browser/IgBlazorSamples.Test/Models/SettingsModel.cs new file mode 100644 index 0000000000..a8a87a16ee --- /dev/null +++ b/browser/IgBlazorSamples.Test/Models/SettingsModel.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IgBlazorSamples.Test.Models +{ + /// + /// Model for the test setting componentsMaps section. + /// + public class ComponentMap + { + public required string Name { get; set; } + + public string? InitialSelector { get; set; } + } + + /// + /// Model for the test setting excludedGroups section. + /// + public class ExcludedGroup + { + public required string Group { get; set; } + } + + /// + /// Model for the test setting excludeSamples section. + /// + public class ExcludedSample + { + public string? Name { get; set; } + + public string? Route { get; set; } + } + + public class SampleTestData + { + public required string ComponentName { get; set; } + + public required string TestSelector { get; set; } + + public required string Route { get; set; } + } +} diff --git a/browser/IgBlazorSamples.Test/ReadMe.md b/browser/IgBlazorSamples.Test/ReadMe.md new file mode 100644 index 0000000000..2fefdb2662 --- /dev/null +++ b/browser/IgBlazorSamples.Test/ReadMe.md @@ -0,0 +1,94 @@ +This documents provides instruction on building and running Samples Browser Tests + +# Setup + +- Instal Blazor **.NET SDK** from this website: +https://dotnet.microsoft.com/learn/aspnet/blazor-tutorial/install + +- If you don't have `pwsh`, you will have to [install Powershell](https://docs.microsoft.com/powershell/scripting/install/installing-powershell) + +## 1. Copying Samples to Browser + +- Open `./browser/IgBlazorSamples.Gulp/` folder + +- Run `npm install` command + +- Run `gulp copySamplesToClient` command + +> NOTE: Above command will copy .razor pages and their .cs files from `./samples/` folder to `./browser/IgBlazorSamples.Client/` and generate required toc file. + +## 2. Install Playwright browsers + +- Open a Powershell terminal in the `./browser/IgBlazorSamples.Test/bin/Debug/net8.0` folder. + +- Run `./playwright.ps1 install chromium` to instal only chromium for Playwright to run. For all browsers just omit the `chromium` argument. + +You can run it from the `./browser/IgBlazorSamples.Test` folder as well, but you will need to prefix the path to it: `bin/Debug/net8.0/playwright.ps1 install` + +For running from **cmd** you will need to use `pwsh ./playwright.ps1 install`. + +For other specific browsers refer to `./playwright.ps1 install --help` + +## 3. Build Samples Browser Solution + +- Open the **IgBlazorSamples.Test.sln** solution + +- Build the solution. Should successfully build all projects. + +## 4. Run tests + +### Test settings + +The `./browser/IgBlazorSamples.Test/` project once built, will generate all the tests and they should appear in the test explorer. +Each test once run will start its own instance of the Samples Browser Client, so no need for any other extra steps than just hitting run/debug on the test. + + +- If the tests use too much resource you can decrease number of workers in the `./browser/.runsettings` or located also in the test solution under `Solution Items/.runsettings`. + +- Disabling headless run for tests locally - uncomment the headless launch option in `.runsettings` + +- If you don't want each test using in memory browser server, you can disable it in the `.runsettings` by setting `useInMemoryClient` to `false`. In this case you will need to run your own instance of the `IgBlazorSamples.Client`, either from VS or using `dotnet run` in the project folder. +You will need to specify the `testUrl` if you use a different the run the `IgBlazorSamples.Client` on. + +### Using Visual Studio + +- In the **Test Explorer** you can run all tests or individually now. + +- Debugging from VS - Right click a test and choose debug. Should have a breakpoint of course in test :P. + +- Debugging with Playwright Inspector - https://playwright.dev/dotnet/docs/debug. Pretty much add `await page.PauseAsync();` to the start of a test or elsewhere and the Playwright inspector will start at that point onward. + +### Using command line + +In order to run the samples tests you need to specify its category and point to the settings of the test, other you will need to speficy each setting manually, like `-- NUnit.NumberOfTestWorkers=2` if you want to have 2 workers active. + +- Running tests: `dotnet test --filter "ErrorTest" --settings:"../.runsettings"` + +- For other methods of running and debugging and all using command line: https://playwright.dev/dotnet/docs/running-tests + +# Resources and guidance + +- For general guidance related to Playwright tests - https://playwright.dev/dotnet/docs/ + +- Generating selectors / tests code using Playwright Codegen - https://playwright.dev/dotnet/docs/codegen-intro + +- Viewing trace of a failed test group(usually artifact from a build) - https://playwright.dev/dotnet/docs/trace-viewer#opening-the-trace + +## Generic sample tests + +The generic tests that run on all samples use parameter passing to the main `ErrorTest`, that parses the `toc.json` from the `IgSamplesBrowser.Client/wwwroot` and goes through it. + +## Test Selector + +Each component from the `toc.json` is recommended to have a specified test selector, which is used for the generic tests to wait to be rendered. This is to be ensure that the sample component is fully(mostly) rendered and the test is actually testing correctly. + +If such test selector is not specified, such would be generated based on the name of the component. + +## Ignoring specific sample + +In order to ignore a specific test from the samples browser, you need to specify it in the `excludeSamples` group in the `testsettings.json`. + +## Ignoring a whole group of test based on the toc + +You will need to specify the group path under `excludedGroups` in the `testsettings.json`. The path is based on the toc group names. +If the name of a group has the `/` symbol, it needs to be ignore with `\\/` for the path to be read correctly. diff --git a/browser/IgBlazorSamples.Test/SampleErrorTests.cs b/browser/IgBlazorSamples.Test/SampleErrorTests.cs new file mode 100644 index 0000000000..7f4c9f1b21 --- /dev/null +++ b/browser/IgBlazorSamples.Test/SampleErrorTests.cs @@ -0,0 +1,56 @@ +using IgBlazorSamples.Test; +using System.Text; + +namespace SampleErrorTests; + +class ErrorTestsData : System.Collections.IEnumerable +{ + public System.Collections.IEnumerator GetEnumerator() { + var data = TestUtils.GetBrowserSamplesBaseInfo(); + foreach(var item in data) + { + yield return new object[] { item.ComponentName, item.Route, item.TestSelector }; + } + } +} + +[Parallelizable(ParallelScope.Self)] +[TestFixtureSource(typeof(ErrorTestsData))] +public class ErrorTest : BlazorTest +{ + private string testSelector; + private string goToUrl; + public ErrorTest(string componentName, string goToUrl, string testSelector) + { + this.goToUrl = String.Format("{0}/{1}", RootUri.AbsoluteUri.TrimEnd('/'), goToUrl.TrimStart('/')); + this.testSelector = testSelector; + } + + [SetUp] + public void InitFixture() + { + Context.SetDefaultTimeout(TestContext.Parameters.Get("defaultTimeout", 30000)); + } + + [Test] + [Category("SampleTest")] + public async Task BasicErrorTest() + { + int numPageErrors = 0; + StringBuilder messages = new(); + Page.Console += (_, value) => + { + if (value.Type == "warning" && value.Text.Contains("Error:")) + { + messages.AppendLine(value.Text); + numPageErrors++; + } + }; + + await Page.GotoAsync(goToUrl); + + await Page.WaitForSelectorAsync(testSelector); + + Assert.That(numPageErrors == 0, $"The following errors were thrown: \n ${messages}"); + } +} diff --git a/browser/IgBlazorSamples.Test/TestUtils.cs b/browser/IgBlazorSamples.Test/TestUtils.cs new file mode 100644 index 0000000000..d2ea057d86 --- /dev/null +++ b/browser/IgBlazorSamples.Test/TestUtils.cs @@ -0,0 +1,87 @@ +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Configuration; +using IgBlazorSamples.Test.Models; +using System.Reflection; +using Infragistics.Samples.Core; +using System.Text.RegularExpressions; + +namespace IgBlazorSamples.Test +{ + public class TestUtils + { + private static IConfiguration config = new ConfigurationBuilder() + .SetBasePath(Path.Combine(Directory.GetCurrentDirectory())) + .AddJsonFile("testsettings.json", optional: false, reloadOnChange: false) + .Build(); + + private static JsonSerializerOptions jsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + public static List GetBrowserSamplesBaseInfo() + { + var toc = GetToc(); + var excludedSamples = config.GetSection("excludeSamples").Get() ?? []; + var excludedGroups = config.GetSection("excludedGroups").Get() ?? []; + var excludedPaths = excludedGroups.Select(r => r.Group.Replace("\\/", "@slash").Split("/", StringSplitOptions.RemoveEmptyEntries) + .Select(e => e.Replace("@slash", "/")) + .ToArray() + ); + + List testsData = new(); + foreach (var group in toc.Groups) + { + var exGroups = excludedPaths.Select(p => p.Length == 1 ? p[0] : "").Where(p => p != ""); + var isValidGroup = !exGroups.Any(exGroup => group.Name == exGroup); + if (isValidGroup) + { + var exComp = excludedPaths.Select(p => p[0] == group.Name && p.Length == 2 ? p[1] : "").Where(p => p != ""); + var groupComponents = group.Components.Where(comp => !exComp.Contains(comp.Name)); + foreach (var comp in groupComponents) + { + var samples = comp.Samples?.Where(s => s.ShowLink) + .Where(s => !excludedSamples.Any(e => e.Name == s.Name && e.Route == s.Route)); + var componentName = comp.Name.Replace(" ", ""); + var componentsMaps = config.GetSection("componentsMaps").Get() ?? []; + var testSelector = componentsMaps.FirstOrDefault(s => s.Name.Replace(" ", "") == componentName)?.InitialSelector; + + if (testSelector == null) + { + Console.WriteLine("Warning: The " + componentName + "does not have test selector defined in testsettings.json. Using a generated one."); + + // Use the componentName to build a test selector. Split by upper cases and then lower all first letters of words. + string[] splitName = Regex.Split(componentName, @"(? subName.Substring(0, 1).ToLower() + subName.Substring(1)).ToArray(); + testSelector = "igc-" + string.Join("-", splitName); + } + + foreach (var sample in samples ?? []) + { + testsData.Add(new() { ComponentName = componentName, TestSelector = testSelector, Route = sample.Route }); + } + }; + } + }; + + return testsData; + } + + private static TOC GetToc() + { + string result = string.Empty; + string toc = "IgBlazorSamples.Test.External.toc.json"; + Assembly assembly = Assembly.GetExecutingAssembly(); + + if (assembly.GetManifestResourceNames().Contains(toc)) + { + using Stream stream = assembly.GetManifestResourceStream(toc)!; + using StreamReader reader = new StreamReader(stream); + result = reader.ReadToEnd(); + } + + return JsonSerializer.Deserialize(result, jsonOptions) ?? new(); + } + } +} diff --git a/browser/IgBlazorSamples.Test/testsettings.json b/browser/IgBlazorSamples.Test/testsettings.json new file mode 100644 index 0000000000..ba8cdb5eee --- /dev/null +++ b/browser/IgBlazorSamples.Test/testsettings.json @@ -0,0 +1,210 @@ +{ + "componentsMaps": [ + { + "name": "Grid", + "initialSelector": "igx-grid-header" + }, + { + "name": "Hierarchical Grid", + "initialSelector": "igx-grid-header" + }, + { + "name": "Pivot Grid", + "initialSelector": "igx-grid-header" + }, + { + "name": "Tree Grid", + "initialSelector": "igx-grid-header" + }, + { + "name": "List", + "initialSelector": "igc-list-item" + }, + { + "name": "Tree", + "initialSelector": "igc-tree-item" + }, + { + "name": "Badge", + "initialSelector": "igc-badge" + }, + { + "name": "Button", + "initialSelector": "igc-button" + }, + { + "name": "Button Group", + "initialSelector": "igc-toggle-button button" + }, + { + "name": "Carousel", + "initialSelector": "igc-carousel-slide" + }, + { + "name": "Checkbox", + "initialSelector": "igc-checkbox input" + }, + { + "name": "Chip", + "initialSelector": "igc-chip button" + }, + { + "name": "Circular Progress Indicator", + "initialSelector": "igc-circular-progress circle" + }, + { + "name": "Combo", + "initialSelector": "igc-combo input" + }, + { + "name": "Date Time Input", + "initialSelector": "igc-date-time-input input" + }, + { + "name": "Dropdown", + "initialSelector": "igc-dropdown-item" + }, + { + "name": "Icon Button", + "initialSelector": "igc-icon-button igc-icon" + }, + { + "name": "Input", + "initialSelector": "igc-input input" + }, + { + "name": "Linear Progress Indicator", + "initialSelector": "igc-linear-progress" + }, + { + "name": "Radio", + "initialSelector": "igc-radio" + }, + { + "name": "Rating", + "initialSelector": "igc-rating-symbol" + }, + { + "name": "Select", + "initialSelector": "igc-select igc-popover igc-input input" + }, + { + "name": "Slider", + "initialSelector": "igc-slider" + }, + { + "name": "Switches", + "initialSelector": "igc-switch input" + }, + { + "name": "Textarea", + "initialSelector": "igc-textarea textarea" + }, + { + "name": "Textarea", + "initialSelector": "igc-textarea textarea" + }, + { + "name": "Accordion", + "initialSelector": "igc-expansion-panel" + }, + { + "name": "Avatar", + "initialSelector": "igc-avatar" + }, + { + "name": "Card", + "initialSelector": "igc-card-content" + }, + { + "name": "Expansion Panel", + "initialSelector": "igc-expansion-panel" + }, + { + "name": "Icon", + "initialSelector": "igc-icon svg" + }, + { + "name": "Stepper", + "initialSelector": "igc-step" + }, + { + "name": "Tabs", + "initialSelector": "igc-tab" + }, + { + "name": "Nav Bar", + "initialSelector": "igc-navbar" + }, + { + "name": "Nav Drawer", + "initialSelector": "igc-nav-drawer" + }, + { + "name": "Banner", + "initialSelector": "igc-banner" + }, + { + "name": "Dialog", + "initialSelector": "igc-dialog dialog" + }, + { + "name": "Snackbar", + "initialSelector": "igc-snackbar" + }, + { + "name": "Toast", + "initialSelector": "igc-toast" + }, + { + "name": "Calendar", + "initialSelector": "igc-calendar igc-days-view" + }, + { + "name": "Date Picker", + "initialSelector": "igc-date-picker igc-date-time-input input" + } + ], + "excludedGroups": [ + { "group": "Charts/" }, + { "group": "Editors/" }, + { "group": "Excel/" }, + { "group": "Gauges/" }, + { "group": "Maps/" }, + { "group": "Inputs/Ripple/" }, + { "group": "Layouts/Dock Manager/" }, + { "group": "Layouts/Divider/" }, + { "group": "Notifications/Dialog/" }, + { "group": "Other \\/ Deprecated Components/" } + ], + "excludeSamples": [ + { + "name": "State Persistence About", + "route": "/grids/grid/state-persistence-about" + }, + { + "name": "State Persistence About", + "route": "/grids/grid/state-persistence-about" + }, + { + "name": "Complex Feature Name1", + "route": "/grids/hierarchical-grid/complex-feature-name1" + }, + { + "name": "Complex Feature Name2", + "route": "/grids/hierarchical-grid/complex-feature-name2" + }, + { + "name": "State Persistence About", + "route": "/grids/hierarchical-grid/state-persistence-about" + }, + { + "name": "State Persistence About", + "route": "/grids/pivot-grid/state-persistence-about" + }, + { + "name": "State Persistence About", + "route": "/grids/tree-grid/state-persistence-about" + } + ] +} diff --git a/browser/IgBlazorSamples.TestServer/IgBlazorSamples.TestServer.csproj b/browser/IgBlazorSamples.TestServer/IgBlazorSamples.TestServer.csproj new file mode 100644 index 0000000000..45e1d7623a --- /dev/null +++ b/browser/IgBlazorSamples.TestServer/IgBlazorSamples.TestServer.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/browser/IgBlazorSamples.TestServer/Pages/Error.cshtml b/browser/IgBlazorSamples.TestServer/Pages/Error.cshtml new file mode 100644 index 0000000000..5e918d2afb --- /dev/null +++ b/browser/IgBlazorSamples.TestServer/Pages/Error.cshtml @@ -0,0 +1,42 @@ +@page +@model BlazorWasmPlaywright.Server.Pages.ErrorModel + + + + + + + + Error + + + + + +
+
+

Error.

+

An error occurred while processing your request.

+ + @if (Model.ShowRequestId) + { +

+ Request ID: @Model.RequestId +

+ } + +

Development Mode

+

+ Swapping to the Development environment displays detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+
+
+ + + diff --git a/browser/IgBlazorSamples.TestServer/Pages/Error.cshtml.cs b/browser/IgBlazorSamples.TestServer/Pages/Error.cshtml.cs new file mode 100644 index 0000000000..15bcf63337 --- /dev/null +++ b/browser/IgBlazorSamples.TestServer/Pages/Error.cshtml.cs @@ -0,0 +1,26 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace BlazorWasmPlaywright.Server.Pages; + +[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] +[IgnoreAntiforgeryToken] +public class ErrorModel : PageModel +{ + public string? RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + private readonly ILogger _logger; + + public ErrorModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } +} diff --git a/browser/IgBlazorSamples.TestServer/Program.cs b/browser/IgBlazorSamples.TestServer/Program.cs new file mode 100644 index 0000000000..90d8396399 --- /dev/null +++ b/browser/IgBlazorSamples.TestServer/Program.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.ResponseCompression; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllersWithViews(); +builder.Services.AddRazorPages(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseWebAssemblyDebugging(); +} +else +{ + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +//app.UseHttpsRedirection(); + +app.UseBlazorFrameworkFiles(); +app.UseStaticFiles(); + +app.UseRouting(); + +app.MapRazorPages(); +app.MapControllers(); +app.MapFallbackToFile("index.html"); + +app.Run(); diff --git a/browser/IgBlazorSamples.TestServer/Properties/AssemblyInfo.cs b/browser/IgBlazorSamples.TestServer/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..b54f4b6797 --- /dev/null +++ b/browser/IgBlazorSamples.TestServer/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("IgBlazorSamples.Test")] diff --git a/browser/IgBlazorSamples.TestServer/Properties/launchSettings.json b/browser/IgBlazorSamples.TestServer/Properties/launchSettings.json new file mode 100644 index 0000000000..b8a0b0c1b0 --- /dev/null +++ b/browser/IgBlazorSamples.TestServer/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:6883/", + "sslPort": 44317 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "SamplesBrowser.Client": { + "commandName": "Project", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:5001;http://localhost:4200/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/browser/IgBlazorSamples.TestServer/ReadMe.md b/browser/IgBlazorSamples.TestServer/ReadMe.md new file mode 100644 index 0000000000..8a1773240e --- /dev/null +++ b/browser/IgBlazorSamples.TestServer/ReadMe.md @@ -0,0 +1,2 @@ +Test Server project compatible with `WebApplicationFactory` tests used by `IgBlazorSamples.Test` project. +The reference to the `IgSamplesBrowser.Client` project allows this one to reuse all razor pages with their routing. \ No newline at end of file diff --git a/browser/IgBlazorSamples.TestServer/appsettings.Development.json b/browser/IgBlazorSamples.TestServer/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/browser/IgBlazorSamples.TestServer/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/browser/IgBlazorSamples.TestServer/appsettings.json b/browser/IgBlazorSamples.TestServer/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/browser/IgBlazorSamples.TestServer/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}