diff --git a/README.md b/README.md index 8c1ae92..bdc0702 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,11 @@ There's also an option to generate a nice html report for all test scenarios. Ju ![image](https://github.com/cezarypiatek/NScenario/assets/7759991/13c501d6-d26b-4406-93fb-1da29dca9bff) + +This report browser supports links to scenario steps definition. To make it work, you need to set the following msbuild properties (or environment variables): +- RepositoryUrl +- SourceRevisionId + ## Test scenario title Test scenario title is generated by removing underscores and splitting camel/pascalcase string from the test method name (`[CallerMemberName]` is used to retrieve that name). This allows for immediate review of the test name (I saw many, extremely long and totally ridiculous test method names. A good test method name should reveal the intention of the test case, not its details). You can always override the default title by setting it explicitly during test scenario creation (especially useful for parametrized test methods): diff --git a/src/NScenario/ReportExtensions.cs b/src/NScenario/ReportExtensions.cs index 529f1a0..5d8a9ae 100644 --- a/src/NScenario/ReportExtensions.cs +++ b/src/NScenario/ReportExtensions.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Reflection; using System.Text.Json; namespace NScenario; @@ -8,8 +10,21 @@ public static class ReportExtensions { public static void SaveAsReport(this IReadOnlyList scenarios, string outputPath) { + var callingAssembly = Assembly.GetCallingAssembly(); + var sourceControlInfo = RepoPathResolver.GetSourceControlInfo(callingAssembly); + var repositoryRootDir = RepoPathResolver.FindRepoRootDirectory(scenarios.FirstOrDefault()?.FilePath); var template = ResourceExtractor.GetEmbeddedResourceContent("NScenario.report-browser-template.html"); - var report = template.Replace("//[DATA_PLACEHOLDER]", JsonSerializer.Serialize(scenarios)); + var scenariosPayload = JsonSerializer.Serialize(new + { + SourceControlInfo = new + { + RepositoryUrl = sourceControlInfo?.RepositoryUrl, + Revision = sourceControlInfo?.Revision, + RepositoryRootDir = repositoryRootDir + }, + Scenarios = scenarios + }); + var report = template.Replace("//[DATA_PLACEHOLDER]", scenariosPayload); File.WriteAllText(outputPath, report); } } \ No newline at end of file diff --git a/src/NScenario/Reporting/RepoPathResolver.cs b/src/NScenario/Reporting/RepoPathResolver.cs new file mode 100644 index 0000000..3107f40 --- /dev/null +++ b/src/NScenario/Reporting/RepoPathResolver.cs @@ -0,0 +1,40 @@ +using System.IO; +using System.Linq; +using System.Reflection; + +namespace NScenario; + +internal static class RepoPathResolver +{ + public static string? FindRepoRootDirectory(string? fileInTheRepo) + { + if (fileInTheRepo == null) + return null; + + var currentDirectory = new DirectoryInfo(Path.GetDirectoryName(fileInTheRepo) ?? string.Empty); + + while (currentDirectory is { Exists: true }) + { + if (Directory.Exists(Path.Combine(currentDirectory.FullName, ".git"))) + { + return currentDirectory.FullName; + } + currentDirectory = currentDirectory.Parent; + } + + return null; + } + + public static SourceControlInfo? GetSourceControlInfo(Assembly assembly) + { + var repositoryUrl = assembly.GetCustomAttributes().FirstOrDefault(x => x.Key == "RepositoryUrl")?.Value; + var revision = assembly.GetCustomAttributes().FirstOrDefault()?.InformationalVersion?.Split('+').LastOrDefault(); + + if (string.IsNullOrWhiteSpace(repositoryUrl) == false && string.IsNullOrWhiteSpace(revision) == false) + { + return new SourceControlInfo(repositoryUrl, revision); + } + + return null; + } +} \ No newline at end of file diff --git a/src/NScenario/Reporting/SourceControlInfo.cs b/src/NScenario/Reporting/SourceControlInfo.cs new file mode 100644 index 0000000..5811d64 --- /dev/null +++ b/src/NScenario/Reporting/SourceControlInfo.cs @@ -0,0 +1,3 @@ +namespace NScenario; + +internal record SourceControlInfo(string RepositoryUrl, string Revision); \ No newline at end of file diff --git a/src/NScenario/report-browser-template.html b/src/NScenario/report-browser-template.html index e3939c3..b51d9cf 100644 --- a/src/NScenario/report-browser-template.html +++ b/src/NScenario/report-browser-template.html @@ -1,4 +1,4 @@ -NScenario - Test Scenarios
+NScenario - Test Scenarios
diff --git a/src/nscenario-report-browser/package.json b/src/nscenario-report-browser/package.json index 1dc53af..60c5efe 100644 --- a/src/nscenario-report-browser/package.json +++ b/src/nscenario-report-browser/package.json @@ -24,7 +24,7 @@ "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "inline": "inliner -nm --preserve-comments build/index.html > build/report-browser-template.html" + "inline": "inliner -nm --preserve-comments build/index.html > ../NScenario/report-browser-template.html" }, "eslintConfig": { "extends": [ diff --git a/src/nscenario-report-browser/public/scenarios.json b/src/nscenario-report-browser/public/scenarios.json index a71de29..01abe92 100644 --- a/src/nscenario-report-browser/public/scenarios.json +++ b/src/nscenario-report-browser/public/scenarios.json @@ -1,394 +1,292 @@ -[ - { - "ScenarioTitle": "should collect info about exceptions", - "MethodName": "should_collect_info_about_exceptions", - "FilePath": "C:\\repos\\NScenario\\src\\UnitTest2.cs", - "LineNumber": 138, - "Status": 1, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 142, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0190945", - "Status": 2, - "Exception": null, - "SubSteps": [ - { - "Description": "This is the first sub-step of first step", - "LineNumber": 144, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0011362", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second sub-step of first step", - "LineNumber": 148, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0100732", - "Status": 2, - "Exception": null, - "SubSteps": [ - { - "Description": "Yet another nesting level p1", - "LineNumber": 150, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0090506", - "Status": 2, - "Exception": "System.InvalidOperationException: Something wrong\r\n at NScenario.Demo.Tests.<>c.b__5_4() in C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs:line 152\r\n at NScenario.StepExecutors.OutputScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\OutputScenarioStepExecutor.cs:line 31\r\n at NScenario.StepExecutors.LevelTrackingScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\LevelTrackingScenarioStepExecutor.cs:line 21\r\n at NScenario.StepExecutors.LevelTrackingScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\LevelTrackingScenarioStepExecutor.cs:line 21\r\n at NScenario.ScenarioInfoCollectorExecutor.Step(String scenarioName, String stepDescription, MaybeAsyncAction action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\ScenarioInfoCollectorExecutor.cs:line 61", - "SubSteps": null - } - ] - } - ] - } - ] +{ + "SourceControlInfo": { + "RepositoryUrl": "https://github.com/cezarypiatek/NScenario", + "Revision": "f05d0f6f5c6c75e7cc2247b94448dbd082440d90", + "RepositoryRootDir": "C:\\repos\\NScenario" }, - { - "ScenarioTitle": "some scenario when flag set to 'False'", - "MethodName": "should_present_basic_scenario_with_explicit_title", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 58, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 60, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0003793", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second step", - "LineNumber": 65, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001727", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the third step", - "LineNumber": 70, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001519", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "ScenarioTitle": "some scenario when flag set to 'True'", - "MethodName": "should_present_basic_scenario_with_explicit_title", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 58, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 60, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0000399", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second step", - "LineNumber": 65, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0000201", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the third step", - "LineNumber": 70, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0000520", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "ScenarioTitle": "should present basic scenario", - "MethodName": "should_present_basic_scenario", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 13, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 15, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0003591", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second step", - "LineNumber": 20, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0092570", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the third step", - "LineNumber": 25, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0008134", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "ScenarioTitle": "should present basic scenario with steps returning value", - "MethodName": "should_present_basic_scenario_with_steps_returning_value", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 79, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 81, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0002627", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second step", - "LineNumber": 87, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0041451", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the third step", - "LineNumber": 94, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001964", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "ScenarioTitle": "should Present BASIC Scenario With Camel Case Title", - "MethodName": "shouldPresentBASICScenarioWithCamelCaseTitle", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 36, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 38, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0002279", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second step", - "LineNumber": 43, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001344", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the third step", - "LineNumber": 48, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001646", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "ScenarioTitle": "should present scenario with sub steps", - "MethodName": "should_present_scenario_with_sub_steps", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 104, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 106, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0025073", - "Status": 3, - "Exception": null, - "SubSteps": [ - { - "Description": "This is the first sub-step of first step", - "LineNumber": 108, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001351", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second sub-step of first step", - "LineNumber": 112, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0012006", - "Status": 3, - "Exception": null, - "SubSteps": [ - { - "Description": "Yet another nesting level p1", - "LineNumber": 114, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001024", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "Yet another nesting level p2", - "LineNumber": 118, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0000864", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - } - ] - }, - { - "Description": "This is the second step", - "LineNumber": 175, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0010105", - "Status": 3, - "Exception": null, - "SubSteps": [ - { - "Description": "This is the first sub-step of second step", - "LineNumber": 177, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0000875", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second sub-step of second step", - "LineNumber": 181, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0000664", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "Description": "This is the third step", - "LineNumber": 127, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0000586", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "ScenarioTitle": "should Present BASIC Scenario With Camel Case Title 1", - "MethodName": "shouldPresentBASICScenarioWithCamelCaseTitle", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 36, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 38, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0002279", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second step", - "LineNumber": 43, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001344", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the third step", - "LineNumber": 48, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001646", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "ScenarioTitle": "should Present BASIC Scenario With Camel Case Title 2", - "MethodName": "shouldPresentBASICScenarioWithCamelCaseTitle", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 36, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 38, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0002279", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second step", - "LineNumber": 43, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001344", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the third step", - "LineNumber": 48, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001646", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - } -] \ No newline at end of file + "Scenarios": [ + { + "ScenarioTitle": "should collect info about exceptions", + "MethodName": "should_collect_info_about_exceptions", + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "LineNumber": 146, + "Status": 1, + "Steps": [ + { + "Description": "This is the first step", + "LineNumber": 150, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.6993193", + "Status": 2, + "Exception": null, + "SubSteps": [ + { + "Description": "This is the first sub-step of first step", + "LineNumber": 152, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0013353", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the second sub-step of first step", + "LineNumber": 156, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.4782463", + "Status": 2, + "Exception": null, + "SubSteps": [ + { + "Description": "Yet another nesting level p1", + "LineNumber": 158, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.2343907", + "Status": 2, + "Exception": "System.InvalidOperationException: Something wrong\r\n at NScenario.Demo.Tests.\u003c\u003ec.\u003cshould_collect_info_about_exceptions\u003eb__5_4() in C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs:line 160\r\n at NScenario.StepExecutors.OutputScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\OutputScenarioStepExecutor.cs:line 31\r\n at NScenario.StepExecutors.LevelTrackingScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\LevelTrackingScenarioStepExecutor.cs:line 21\r\n at NScenario.StepExecutors.LevelTrackingScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\LevelTrackingScenarioStepExecutor.cs:line 21\r\n at NScenario.ScenarioInfoCollectorExecutor.Step(String scenarioName, String stepDescription, MaybeAsyncAction action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\ScenarioInfoCollectorExecutor.cs:line 61", + "SubSteps": null + } + ] + } + ] + } + ] + }, + { + "ScenarioTitle": "some scenario when flag set to \u0027False\u0027", + "MethodName": "should_present_basic_scenario_with_explicit_title", + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "LineNumber": 59, + "Status": 0, + "Steps": [ + { + "Description": "This is the first step", + "LineNumber": 61, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001056", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the second step", + "LineNumber": 66, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001212", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the third step", + "LineNumber": 71, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001092", + "Status": 3, + "Exception": null, + "SubSteps": null + } + ] + }, + { + "ScenarioTitle": "some scenario when flag set to \u0027True\u0027", + "MethodName": "should_present_basic_scenario_with_explicit_title", + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "LineNumber": 59, + "Status": 0, + "Steps": [ + { + "Description": "This is the first step", + "LineNumber": 61, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0000481", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the second step", + "LineNumber": 66, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0000052", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the third step", + "LineNumber": 71, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0000042", + "Status": 3, + "Exception": null, + "SubSteps": null + } + ] + }, + { + "ScenarioTitle": "should present basic scenario", + "MethodName": "should_present_basic_scenario", + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "LineNumber": 14, + "Status": 0, + "Steps": [ + { + "Description": "This is the first step", + "LineNumber": 16, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001828", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the second step", + "LineNumber": 21, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001153", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the third step", + "LineNumber": 26, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001128", + "Status": 3, + "Exception": null, + "SubSteps": null + } + ] + }, + { + "ScenarioTitle": "should present basic scenario with steps returning value", + "MethodName": "should_present_basic_scenario_with_steps_returning_value", + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "LineNumber": 80, + "Status": 0, + "Steps": [ + { + "Description": "This is the first step", + "LineNumber": 82, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0003630", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the second step", + "LineNumber": 88, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0046366", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the third step", + "LineNumber": 95, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0002009", + "Status": 3, + "Exception": null, + "SubSteps": null + } + ] + }, + { + "ScenarioTitle": "should Present BASIC Scenario With Camel Case Title", + "MethodName": "shouldPresentBASICScenarioWithCamelCaseTitle", + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "LineNumber": 37, + "Status": 0, + "Steps": [ + { + "Description": "This is the first step", + "LineNumber": 39, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001102", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the second step", + "LineNumber": 44, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0000801", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the third step", + "LineNumber": 49, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0000750", + "Status": 3, + "Exception": null, + "SubSteps": null + } + ] + }, + { + "ScenarioTitle": "should present scenario with sub steps", + "MethodName": "should_present_scenario_with_sub_steps", + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "LineNumber": 106, + "Status": 1, + "Steps": [ + { + "Description": "This is the first step", + "LineNumber": 108, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.7567549", + "Status": 2, + "Exception": null, + "SubSteps": [ + { + "Description": "This is the first sub-step of first step", + "LineNumber": 110, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001420", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the second sub-step of first step", + "LineNumber": 114, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.5376479", + "Status": 2, + "Exception": null, + "SubSteps": [ + { + "Description": "Yet another nesting level p1", + "LineNumber": 116, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001048", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "Yet another nesting level p2", + "LineNumber": 120, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.2110651", + "Status": 2, + "Exception": "System.InvalidOperationException: Hello\r\n at NScenario.Demo.Tests.\u003c\u003ec.\u003cshould_present_scenario_with_sub_steps\u003eb__4_5() in C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs:line 123\r\n at NScenario.StepExecutors.OutputScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\OutputScenarioStepExecutor.cs:line 31\r\n at NScenario.StepExecutors.LevelTrackingScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\LevelTrackingScenarioStepExecutor.cs:line 21\r\n at NScenario.StepExecutors.LevelTrackingScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\LevelTrackingScenarioStepExecutor.cs:line 21\r\n at NScenario.ScenarioInfoCollectorExecutor.Step(String scenarioName, String stepDescription, MaybeAsyncAction action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\ScenarioInfoCollectorExecutor.cs:line 61", + "SubSteps": null + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/nscenario-report-browser/src/App.tsx b/src/nscenario-report-browser/src/App.tsx index 951de33..4e4c7dd 100644 --- a/src/nscenario-report-browser/src/App.tsx +++ b/src/nscenario-report-browser/src/App.tsx @@ -1,15 +1,16 @@ -import React, {useEffect, useState} from 'react'; +import React, {useContext, useEffect, useState} from 'react'; import './App.css'; //import data from "./scenarios.json" import DirectoryTree from 'antd/es/tree/DirectoryTree'; import { DataNode } from 'antd/es/tree'; -import { Card, Col, Row, Space, Statistic, Timeline} from "antd"; +import { Card, Col, Row, Statistic, Timeline} from "antd"; import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'; +import {ICodeLocation, RepoPathResolver} from "./External"; @@ -107,15 +108,27 @@ function findLongestCommonPrefix(strings: string[]): string { return prefix; } +enum TestResultType{ + All, + Success, + Failed +} + +export function StatisticsCtr(props: {scenarios:Scenario[], onSetFilter: (type:TestResultType) => void }) { + var success = props.scenarios.filter(value => value.Status === 0).length; + var failed = props.scenarios.filter(value => value.Status === 1).length; + const [typeState, setTypeState] = useState(TestResultType.All) + const setFilter = (type:TestResultType) => { + type = typeState === type? TestResultType.All: type; + props.onSetFilter(type); + setTypeState(type); + }; -export function StatisticsCtr(props: {scenarios:Scenario[]}) { - var success = props.scenarios.filter(value => value.Status === 0).length - var failed = props.scenarios.filter(value => value.Status === 1).length return ( - + - + setFilter(TestResultType.All)} className={typeState === TestResultType.All ? "selected-filter":""}> - + setFilter(TestResultType.Success)} className={typeState === TestResultType.Success ? "selected-filter":""}> - + setFilter(TestResultType.Failed)} className={typeState === TestResultType.Failed ? "selected-filter":""}> Step {props.prefix}: {`${props.data.Description}`} + <> Step {props.prefix}: {props.data.Description} + + {props.data.Exception != null && ( +

+              {props.data.Exception}
+          
+ )} {props.data.SubSteps && {props.data.Status === 0 ? () :} - { `SCENARIO: ${props.data.ScenarioTitle}`} + SCENARIO: {props.data.ScenarioTitle} ); } const ScenarioCtr = React.memo((props: {data:Scenario, isSelected:boolean}) =>{ return ( - )} style={{width:"auto", border: props.isSelected? "1px solid blue": "1px solid transparent", transition: "border-color 0.5s ease" }}> + )} style={{width:"100%", border: props.isSelected? "1px solid blue": "1px solid transparent", transition: "border-color 0.5s ease", marginBottom: "20px" }}> 0) { try { - return JSON.parse(dataStorage.innerText) as Scenario[]; + return JSON.parse(dataStorage.innerText) as INScenarioData; }catch { - return []; } } - return []; + return { + SourceControlInfo: {Revision: null, RepositoryUrl: null, RepositoryRootDir: null}, + Scenarios: [] + }; +} + +interface ISourceControlInfo +{ + RepositoryUrl:string | null, + Revision:string | null, + RepositoryRootDir:string | null } +interface INScenarioData +{ + SourceControlInfo: ISourceControlInfo, + Scenarios: Scenario[] +} + +interface GlobalServices{ + pathResolver: ((location: ICodeLocation) => string) +} + +const GlobalServicesContext = React.createContext({pathResolver: (location)=> location.FilePath }) + + function App() { - const scenarioData = retriveData(); - const treeData = generateTableOfContent(scenarioData); + const scenarioData = retrieveData(); + const treeData = generateTableOfContent(scenarioData.Scenarios); - const [scenarioState, setScenarioState] = useState(scenarioData) + const [scenarioState, setScenarioState] = useState(scenarioData.Scenarios) const [treeState, setTreeState] = useState(treeData) const [selectedScenario, setSelectedScenario] = useState("") + const [sourceControlState, setSourceControlState] = useState(scenarioData.SourceControlInfo) + const [typeState, setTypeState] = useState(TestResultType.All) + let pathResolver = RepoPathResolver.TryToGetPathBuilder(sourceControlState, sourceControlState.RepositoryRootDir) + useEffect( () => { (async ()=>{ - if(scenarioData.length === 0) + if(scenarioData.Scenarios.length === 0) { const response = await fetch("/scenarios.json") - const sampleData = await response.json(); - setScenarioState(sampleData as Scenario[]) - setTreeState(generateTableOfContent(sampleData)) + const sampleData : INScenarioData = await response.json(); + setScenarioState(sampleData.Scenarios); + setSourceControlState(sampleData.SourceControlInfo); + setTreeState(generateTableOfContent(sampleData.Scenarios)) } })() }); return ( -
+
+ { @@ -241,12 +292,10 @@ function App() { - + { setTypeState(type) }} /> - - {scenarioState.map(value => )} - + {scenarioState.map(value => )} @@ -255,7 +304,7 @@ function App() { - +
); } diff --git a/src/nscenario-report-browser/src/External.ts b/src/nscenario-report-browser/src/External.ts new file mode 100644 index 0000000..83a90e9 --- /dev/null +++ b/src/nscenario-report-browser/src/External.ts @@ -0,0 +1,85 @@ +type SourceControlInfo = { + RepositoryUrl: string | null; + Revision: string | null; +}; + +type SourceControlPathBuilder = (filePath: string, line: number) => string; + +interface ISourceControlPathBuilderFactory { + TryToBuild(sourceControlInfo: SourceControlInfo): SourceControlPathBuilder | null; +} + +class GithubPathBuilderFactory implements ISourceControlPathBuilderFactory { + public TryToBuild(sourceControlInfo: SourceControlInfo): SourceControlPathBuilder | null { + const httpPattern = new RegExp(`https?://github\\.com/(?[^/]+)/(?[^/]+)`); + const httpMatch = httpPattern.exec(sourceControlInfo.RepositoryUrl || ""); + if (httpMatch && httpMatch.groups ) { + return (filePath, line) => `https://github.com/${httpMatch.groups?.['user']}/${httpMatch.groups?.['repo']}/blob/${sourceControlInfo.Revision}/${filePath}#L${line}`; + } + + const sshPattern = new RegExp(`(?:ssh://)?git@github\\.com:(?[^/]+)/(?[^.]+)\\.git`); + const sshMatch = sshPattern.exec(sourceControlInfo.RepositoryUrl || ""); + if (sshMatch && sshMatch.groups) { + return (filePath, line) => `https://github.com/${sshMatch.groups?.['user']}/${sshMatch.groups?.['repo']}/blob/${sourceControlInfo.Revision}/${filePath}#L${line}`; + } + + return null; + } +} + +class BitbucketServerPathBuilderFactory implements ISourceControlPathBuilderFactory +{ + TryToBuild(sourceControlInfo: SourceControlInfo): SourceControlPathBuilder | null { + const httpPattern = new RegExp(`https?://(?[^/]+)/scm/(?[^/]+)/(?[^.]+)\\.git`); + const httpMatch = httpPattern.exec(sourceControlInfo.RepositoryUrl || ""); + if (httpMatch && httpMatch.groups ) { + return (filePath, line) => `https://${httpMatch.groups?.['domain']}/projects/${httpMatch.groups?.['user']}/repos/${httpMatch.groups?.['repo']}/browse/${filePath}?at=${sourceControlInfo.Revision}#${line}`; + } + + const sshPattern = new RegExp(`(?:ssh://)?git@(?[^/]+)/(?[^/]+)/(?[^.]+)\\.git`); + const sshMatch = sshPattern.exec(sourceControlInfo.RepositoryUrl || ""); + if (sshMatch && sshMatch.groups) { + return (filePath, line) => `https://${sshMatch.groups?.['domain']}/projects/${sshMatch.groups?.['user']}/repos/${sshMatch.groups?.['repo']}/browse/${filePath}?at=${sourceControlInfo.Revision}#${line}`; + } + + return null; + } + +} + +export interface ICodeLocation { + FilePath: string; + LineNumber: number; +} + + +export class RepoPathResolver{ + private static readonly _factories: ISourceControlPathBuilderFactory[] = [ + new GithubPathBuilderFactory(), + new BitbucketServerPathBuilderFactory() + ]; + + private static MakePathRelative(rootPath: string, absolutePath: string): string { + const rootUrl = new URL(rootPath, 'file://'); + const absoluteUrl = new URL(absolutePath, 'file://'); + const relativePath = absoluteUrl.href.substring(rootUrl.href.length); + return relativePath.startsWith('/') ? relativePath.substring(1) : relativePath; + } + + public static TryToGetPathBuilder(sourceControlInfo: SourceControlInfo, repoRootPath:string | null) : ((location: ICodeLocation) => string ){ + if(sourceControlInfo.RepositoryUrl != null && sourceControlInfo.Revision != null && repoRootPath != null) + { + for (const factory of this._factories) { + const builder = factory.TryToBuild(sourceControlInfo); + if (builder){ + return (location:ICodeLocation) => { + const relativePath = RepoPathResolver.MakePathRelative(repoRootPath, location.FilePath); + return builder(relativePath, location.LineNumber); + } + } + } + } + + return location => location.FilePath; + } +} \ No newline at end of file diff --git a/src/nscenario-report-browser/src/index.css b/src/nscenario-report-browser/src/index.css index af357d8..d76defd 100644 --- a/src/nscenario-report-browser/src/index.css +++ b/src/nscenario-report-browser/src/index.css @@ -14,4 +14,22 @@ code { .ant-timeline-item { padding-bottom: 5px!important; -} \ No newline at end of file +} + +.filter-row .ant-card +{ + cursor: pointer; +} +.filter-row .ant-card.selected-filter +{ + border: 1px solid blue; +} + +.App.filter-1 .scenario-1 +{ + display: none; +} +.App.filter-2 .scenario-0 +{ + display: none; +}