From aa5ec60577d61babaaf6a881ba9f33032846330d Mon Sep 17 00:00:00 2001 From: kaycan26 Date: Wed, 19 Nov 2025 11:54:51 +0000 Subject: [PATCH 1/6] Added GitHub Actions CI pipeline --- .../BPCalculator.BddTests.csproj | 36 ++++++++ .../BloodPressureCategory.feature | 17 ++++ .../BloodPressureCategoryBddTests.cs | 30 ++++++ .../BloodPressureCategorySteps.cs | 49 ++++++++++ .../BPCalculator.UnitTests.csproj | 29 ++++++ .../BloodPressureCategoryTests.cs | 30 ++++++ BPCalculator.sln | 54 ++++++++++- BPCalculator/BloodPressure.cs | 24 ++++- BPCalculator/Pages/Index.cshtml | 91 +++++++++++++------ ci.yml | 0 10 files changed, 328 insertions(+), 32 deletions(-) create mode 100644 BPCalculator.BddTests/BPCalculator.BddTests.csproj create mode 100644 BPCalculator.BddTests/BloodPressureCategory.feature create mode 100644 BPCalculator.BddTests/BloodPressureCategoryBddTests.cs create mode 100644 BPCalculator.BddTests/BloodPressureCategorySteps.cs create mode 100644 BPCalculator.UnitTests/BPCalculator.UnitTests.csproj create mode 100644 BPCalculator.UnitTests/BloodPressureCategoryTests.cs create mode 100644 ci.yml diff --git a/BPCalculator.BddTests/BPCalculator.BddTests.csproj b/BPCalculator.BddTests/BPCalculator.BddTests.csproj new file mode 100644 index 0000000..9ae05dc --- /dev/null +++ b/BPCalculator.BddTests/BPCalculator.BddTests.csproj @@ -0,0 +1,36 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BPCalculator.BddTests/BloodPressureCategory.feature b/BPCalculator.BddTests/BloodPressureCategory.feature new file mode 100644 index 0000000..009416e --- /dev/null +++ b/BPCalculator.BddTests/BloodPressureCategory.feature @@ -0,0 +1,17 @@ +Feature: Blood pressure category + In order to understand my blood pressure + As a patient + I want to see the correct category for my readings + + Scenario Outline: Categorise blood pressure + Given a systolic value of + And a diastolic value of + When I calculate the blood pressure category + Then the result should be + + Examples: + | Systolic | Diastolic | Category | + | 90 | 60 | Ideal | + | 110 | 70 | Ideal | + | 135 | 85 | PreHigh | + | 150 | 95 | High | diff --git a/BPCalculator.BddTests/BloodPressureCategoryBddTests.cs b/BPCalculator.BddTests/BloodPressureCategoryBddTests.cs new file mode 100644 index 0000000..09c0351 --- /dev/null +++ b/BPCalculator.BddTests/BloodPressureCategoryBddTests.cs @@ -0,0 +1,30 @@ +using BPCalculator; +using Xunit; + +namespace BPCalculator.BddTests +{ + public class BloodPressureCategoryBddTests + { + [Theory(DisplayName = "BDD: Blood pressure category scenarios")] + [InlineData(90, 60, BPCategory.Ideal)] + [InlineData(110, 70, BPCategory.Ideal)] + [InlineData(135, 85, BPCategory.PreHigh)] + [InlineData(150, 95, BPCategory.High)] + public void BloodPressureCategory_Scenarios( + int systolic, int diastolic, BPCategory expectedCategory) + { + // GIVEN a blood pressure reading + var bp = new BloodPressure + { + Systolic = systolic, + Diastolic = diastolic + }; + + // WHEN I calculate the category + var actual = bp.Category; + + // THEN the result should match the expected category + Assert.Equal(expectedCategory, actual); + } + } +} diff --git a/BPCalculator.BddTests/BloodPressureCategorySteps.cs b/BPCalculator.BddTests/BloodPressureCategorySteps.cs new file mode 100644 index 0000000..4c4f675 --- /dev/null +++ b/BPCalculator.BddTests/BloodPressureCategorySteps.cs @@ -0,0 +1,49 @@ +using BPCalculator; +using FluentAssertions; +using TechTalk.SpecFlow; + +namespace BPCalculator.BddTests.StepDefinitions +{ + [Binding] + public class BloodPressureCategorySteps + { + private readonly ScenarioContext _scenarioContext; + + public BloodPressureCategorySteps(ScenarioContext scenarioContext) + { + _scenarioContext = scenarioContext; + } + + [Given(@"a systolic value of (.*)")] + public void GivenASystolicValueOf(int systolic) + { + var bp = new BloodPressure + { + Systolic = systolic + }; + + _scenarioContext["bp"] = bp; + } + + [Given(@"a diastolic value of (.*)")] + public void GivenADiastolicValueOf(int diastolic) + { + var bp = (BloodPressure)_scenarioContext["bp"]; + bp.Diastolic = diastolic; + } + + [When(@"I calculate the blood pressure category")] + public void WhenICalculateTheBloodPressureCategory() + { + var bp = (BloodPressure)_scenarioContext["bp"]; + _scenarioContext["category"] = bp.Category; + } + + [Then(@"the result should be (.*)")] + public void ThenTheResultShouldBe(string expectedCategory) + { + var actual = (BPCategory)_scenarioContext["category"]; + actual.ToString().Should().Be(expectedCategory); + } + } +} diff --git a/BPCalculator.UnitTests/BPCalculator.UnitTests.csproj b/BPCalculator.UnitTests/BPCalculator.UnitTests.csproj new file mode 100644 index 0000000..e6a66fd --- /dev/null +++ b/BPCalculator.UnitTests/BPCalculator.UnitTests.csproj @@ -0,0 +1,29 @@ + + + + net9.0 + enable + enable + false + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + diff --git a/BPCalculator.UnitTests/BloodPressureCategoryTests.cs b/BPCalculator.UnitTests/BloodPressureCategoryTests.cs new file mode 100644 index 0000000..43f14c1 --- /dev/null +++ b/BPCalculator.UnitTests/BloodPressureCategoryTests.cs @@ -0,0 +1,30 @@ +using BPCalculator; +using Xunit; + +namespace BPCalculator.UnitTests +{ + public class BloodPressureCategoryTests + { + [Theory] + [InlineData(90, 60, BPCategory.Ideal)] + [InlineData(110, 70, BPCategory.Ideal)] + [InlineData(135, 85, BPCategory.PreHigh)] + [InlineData(150, 95, BPCategory.High)] + public void Category_Is_Calculated_Correctly( + int systolic, int diastolic, BPCategory expectedCategory) + { + // Arrange + var bp = new BloodPressure + { + Systolic = systolic, + Diastolic = diastolic + }; + + // Act + var actual = bp.Category; + + // Assert + Assert.Equal(expectedCategory, actual); + } + } +} diff --git a/BPCalculator.sln b/BPCalculator.sln index 652d73a..38be67b 100644 --- a/BPCalculator.sln +++ b/BPCalculator.sln @@ -1,24 +1,74 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30002.166 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36518.9 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BPCalculator", "BPCalculator\BPCalculator.csproj", "{5966DA6C-819D-4303-ADC7-425B8541EC84}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BPCalculator.UnitTests", "BPCalculator.UnitTests\BPCalculator.UnitTests.csproj", "{A94AE3F9-BB5C-4B0D-ABE3-8C6446368DC4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BPCalculator.BddTests", "BPCalculator.BddTests\BPCalculator.BddTests.csproj", "{59819ED5-D778-4737-8D70-9A61494224ED}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{478B5C70-9FEE-464F-B914-E80DA228D2D7}" + ProjectSection(SolutionItems) = preProject + ci.yml = ci.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {5966DA6C-819D-4303-ADC7-425B8541EC84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5966DA6C-819D-4303-ADC7-425B8541EC84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5966DA6C-819D-4303-ADC7-425B8541EC84}.Debug|x64.ActiveCfg = Debug|Any CPU + {5966DA6C-819D-4303-ADC7-425B8541EC84}.Debug|x64.Build.0 = Debug|Any CPU + {5966DA6C-819D-4303-ADC7-425B8541EC84}.Debug|x86.ActiveCfg = Debug|Any CPU + {5966DA6C-819D-4303-ADC7-425B8541EC84}.Debug|x86.Build.0 = Debug|Any CPU {5966DA6C-819D-4303-ADC7-425B8541EC84}.Release|Any CPU.ActiveCfg = Release|Any CPU {5966DA6C-819D-4303-ADC7-425B8541EC84}.Release|Any CPU.Build.0 = Release|Any CPU + {5966DA6C-819D-4303-ADC7-425B8541EC84}.Release|x64.ActiveCfg = Release|Any CPU + {5966DA6C-819D-4303-ADC7-425B8541EC84}.Release|x64.Build.0 = Release|Any CPU + {5966DA6C-819D-4303-ADC7-425B8541EC84}.Release|x86.ActiveCfg = Release|Any CPU + {5966DA6C-819D-4303-ADC7-425B8541EC84}.Release|x86.Build.0 = Release|Any CPU + {A94AE3F9-BB5C-4B0D-ABE3-8C6446368DC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A94AE3F9-BB5C-4B0D-ABE3-8C6446368DC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A94AE3F9-BB5C-4B0D-ABE3-8C6446368DC4}.Debug|x64.ActiveCfg = Debug|Any CPU + {A94AE3F9-BB5C-4B0D-ABE3-8C6446368DC4}.Debug|x64.Build.0 = Debug|Any CPU + {A94AE3F9-BB5C-4B0D-ABE3-8C6446368DC4}.Debug|x86.ActiveCfg = Debug|Any CPU + {A94AE3F9-BB5C-4B0D-ABE3-8C6446368DC4}.Debug|x86.Build.0 = Debug|Any CPU + {A94AE3F9-BB5C-4B0D-ABE3-8C6446368DC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A94AE3F9-BB5C-4B0D-ABE3-8C6446368DC4}.Release|Any CPU.Build.0 = Release|Any CPU + {A94AE3F9-BB5C-4B0D-ABE3-8C6446368DC4}.Release|x64.ActiveCfg = Release|Any CPU + {A94AE3F9-BB5C-4B0D-ABE3-8C6446368DC4}.Release|x64.Build.0 = Release|Any CPU + {A94AE3F9-BB5C-4B0D-ABE3-8C6446368DC4}.Release|x86.ActiveCfg = Release|Any CPU + {A94AE3F9-BB5C-4B0D-ABE3-8C6446368DC4}.Release|x86.Build.0 = Release|Any CPU + {59819ED5-D778-4737-8D70-9A61494224ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59819ED5-D778-4737-8D70-9A61494224ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59819ED5-D778-4737-8D70-9A61494224ED}.Debug|x64.ActiveCfg = Debug|Any CPU + {59819ED5-D778-4737-8D70-9A61494224ED}.Debug|x64.Build.0 = Debug|Any CPU + {59819ED5-D778-4737-8D70-9A61494224ED}.Debug|x86.ActiveCfg = Debug|Any CPU + {59819ED5-D778-4737-8D70-9A61494224ED}.Debug|x86.Build.0 = Debug|Any CPU + {59819ED5-D778-4737-8D70-9A61494224ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59819ED5-D778-4737-8D70-9A61494224ED}.Release|Any CPU.Build.0 = Release|Any CPU + {59819ED5-D778-4737-8D70-9A61494224ED}.Release|x64.ActiveCfg = Release|Any CPU + {59819ED5-D778-4737-8D70-9A61494224ED}.Release|x64.Build.0 = Release|Any CPU + {59819ED5-D778-4737-8D70-9A61494224ED}.Release|x86.ActiveCfg = Release|Any CPU + {59819ED5-D778-4737-8D70-9A61494224ED}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {478B5C70-9FEE-464F-B914-E80DA228D2D7} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0888B091-BD2A-49F1-9C68-09927D2EC2D8} EndGlobalSection diff --git a/BPCalculator/BloodPressure.cs b/BPCalculator/BloodPressure.cs index 3e8fee9..ce9f8fa 100644 --- a/BPCalculator/BloodPressure.cs +++ b/BPCalculator/BloodPressure.cs @@ -31,9 +31,27 @@ public BPCategory Category { get { - // implement as part of project - //throw new NotImplementedException("not implemented yet"); - return new BPCategory(); // replace this + // High blood pressure + if (Systolic >= 140 || Diastolic >= 90) + { + return BPCategory.High; + } + + // Pre-high blood pressure + if ((Systolic >= 120 && Systolic <= 139) || + (Diastolic >= 80 && Diastolic <= 89)) + { + return BPCategory.PreHigh; + } + + // ideal blood pressure + if (Systolic >= 90 && Systolic <= 119 && + Diastolic >= 60 && Diastolic <= 79) + { + return BPCategory.Ideal; + } + // otherwise, Low blood pressure + return BPCategory.Low; } } } diff --git a/BPCalculator/Pages/Index.cshtml b/BPCalculator/Pages/Index.cshtml index 26fd873..ab44c3c 100644 --- a/BPCalculator/Pages/Index.cshtml +++ b/BPCalculator/Pages/Index.cshtml @@ -1,40 +1,77 @@ @page @model BPCalculator.Pages.BloodPressureModel - @{ ViewData["Title"] = "BP Category Calculator"; } +@{ + string resultClass = ""; + + if (ViewData.ModelState.IsValid && Model.BP != null) + { + switch (Model.BP.Category) + { + case BPCategory.Low: + resultClass = "alert alert-primary"; // blue + break; + case BPCategory.Ideal: + resultClass = "alert alert-success"; // green + break; + case BPCategory.PreHigh: + resultClass = "alert alert-warning"; // orange + break; + case BPCategory.High: + resultClass = "alert alert-danger"; // red + break; + default: + resultClass = "alert alert-secondary"; + break; + } + } +}

BP Category Calculator


+
-
-
-
- - - -
-
- - - -
-
- + +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ +
+ + + + @if (ViewData.ModelState.IsValid && Model.BP !=null) + { +
+ +
+ @Model.BP.Category +
+
+ }
- @if (ViewData.ModelState.IsValid) - { -
- @Html.DisplayFor(model => model.BP.Category, new { htmlAttributes = new { @class = "form-control" } }) -
- } - -
-
+
-@section Scripts { - @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} -} +
+ diff --git a/ci.yml b/ci.yml new file mode 100644 index 0000000..e69de29 From ae6179ca17edb7961b1b67b7b3a87b966d21412d Mon Sep 17 00:00:00 2001 From: kaycan26 Date: Fri, 21 Nov 2025 15:57:34 +0000 Subject: [PATCH 2/6] Add SonarCloud CI workflow --- BPCalculator.BddTests/BMICalculator.feature | 23 +++ .../BMICalculator.feature.cs | 177 ++++++++++++++++++ BPCalculator.BddTests/BMICalculatorSteps.cs | 43 +++++ .../BPCalculator.PlaywrightTests.csproj | 30 +++ .../BmiPagePlaywrightTests.cs | 68 +++++++ BPCalculator.PlaywrightTests/UnitTest1.cs | 11 ++ .../BPCalculator.SeleniumTests.vbproj | 27 +++ BPCalculator.SeleniumTests/UnitTest1.vb | 63 +++++++ BPCalculator.UnitTests/BMICalculatorTests.cs | 51 +++++ BPCalculator.sln | 31 ++- BPCalculator/BMICalculator.cs | 21 +++ BPCalculator/Pages/Index.cshtml | 94 +++++++--- BPCalculator/Pages/Index.cshtml.cs | 35 +++- ci.yml | 69 +++++++ k6/bp_performance_test.js | 24 +++ sonar-ci.yml | 69 +++++++ 16 files changed, 806 insertions(+), 30 deletions(-) create mode 100644 BPCalculator.BddTests/BMICalculator.feature create mode 100644 BPCalculator.BddTests/BMICalculator.feature.cs create mode 100644 BPCalculator.BddTests/BMICalculatorSteps.cs create mode 100644 BPCalculator.PlaywrightTests/BPCalculator.PlaywrightTests.csproj create mode 100644 BPCalculator.PlaywrightTests/BmiPagePlaywrightTests.cs create mode 100644 BPCalculator.PlaywrightTests/UnitTest1.cs create mode 100644 BPCalculator.SeleniumTests/BPCalculator.SeleniumTests.vbproj create mode 100644 BPCalculator.SeleniumTests/UnitTest1.vb create mode 100644 BPCalculator.UnitTests/BMICalculatorTests.cs create mode 100644 BPCalculator/BMICalculator.cs create mode 100644 k6/bp_performance_test.js create mode 100644 sonar-ci.yml diff --git a/BPCalculator.BddTests/BMICalculator.feature b/BPCalculator.BddTests/BMICalculator.feature new file mode 100644 index 0000000..15aaa86 --- /dev/null +++ b/BPCalculator.BddTests/BMICalculator.feature @@ -0,0 +1,23 @@ +Feature: BMI Calculator + In order to know my BMI category + As a user + I want the system to calculate BMI correctly + + Scenario Outline: BMI category scenarios + Given a height of cm + And a weight of kg + When I calculate BMI + Then the BMI category should be "" + + Examples: + | heightCm | weightKg | expectedCategory | + | 170 | 50 | Underweight | + | 170 | 65 | Normal | + | 170 | 80 | Overweight | + | 170 | 100 | Obese | + + Scenario: BMI borderline between Normal and Overweight + Given a height of 170 cm + And a weight of 72 kg + When I calculate BMI + Then the BMI category should be "Normal" diff --git a/BPCalculator.BddTests/BMICalculator.feature.cs b/BPCalculator.BddTests/BMICalculator.feature.cs new file mode 100644 index 0000000..ab1ebf9 --- /dev/null +++ b/BPCalculator.BddTests/BMICalculator.feature.cs @@ -0,0 +1,177 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by SpecFlow (https://www.specflow.org/). +// SpecFlow Version:3.9.0.0 +// SpecFlow Generator Version:3.9.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +#region Designer generated code +#pragma warning disable +namespace BPCalculator.BddTests +{ + using TechTalk.SpecFlow; + using System; + using System.Linq; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public partial class BMICalculatorFeature : object, Xunit.IClassFixture, System.IDisposable + { + + private static TechTalk.SpecFlow.ITestRunner testRunner; + + private static string[] featureTags = ((string[])(null)); + + private Xunit.Abstractions.ITestOutputHelper _testOutputHelper; + +#line 1 "BMICalculator.feature" +#line hidden + + public BMICalculatorFeature(BMICalculatorFeature.FixtureData fixtureData, BPCalculator_BddTests_XUnitAssemblyFixture assemblyFixture, Xunit.Abstractions.ITestOutputHelper testOutputHelper) + { + this._testOutputHelper = testOutputHelper; + this.TestInitialize(); + } + + public static void FeatureSetup() + { + testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "", "BMI Calculator", " In order to know my BMI category\r\n As a user\r\n I want the system to calculate" + + " BMI correctly", ProgrammingLanguage.CSharp, featureTags); + testRunner.OnFeatureStart(featureInfo); + } + + public static void FeatureTearDown() + { + testRunner.OnFeatureEnd(); + testRunner = null; + } + + public void TestInitialize() + { + } + + public void TestTearDown() + { + testRunner.OnScenarioEnd(); + } + + public void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) + { + testRunner.OnScenarioInitialize(scenarioInfo); + testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(_testOutputHelper); + } + + public void ScenarioStart() + { + testRunner.OnScenarioStart(); + } + + public void ScenarioCleanup() + { + testRunner.CollectScenarioErrors(); + } + + void System.IDisposable.Dispose() + { + this.TestTearDown(); + } + + [Xunit.SkippableTheoryAttribute(DisplayName="BMI category scenarios")] + [Xunit.TraitAttribute("FeatureTitle", "BMI Calculator")] + [Xunit.TraitAttribute("Description", "BMI category scenarios")] + [Xunit.InlineDataAttribute("170", "50", "Underweight", new string[0])] + [Xunit.InlineDataAttribute("170", "65", "Normal", new string[0])] + [Xunit.InlineDataAttribute("170", "80", "Overweight", new string[0])] + [Xunit.InlineDataAttribute("170", "100", "Obese", new string[0])] + public void BMICategoryScenarios(string heightCm, string weightKg, string expectedCategory, string[] exampleTags) + { + string[] tagsOfScenario = exampleTags; + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + argumentsOfScenario.Add("heightCm", heightCm); + argumentsOfScenario.Add("weightKg", weightKg); + argumentsOfScenario.Add("expectedCategory", expectedCategory); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("BMI category scenarios", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 6 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 7 + testRunner.Given(string.Format("a height of {0} cm", heightCm), ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden +#line 8 + testRunner.And(string.Format("a weight of {0} kg", weightKg), ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 9 + testRunner.When("I calculate BMI", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 10 + testRunner.Then(string.Format("the BMI category should be \"{0}\"", expectedCategory), ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="BMI borderline between Normal and Overweight")] + [Xunit.TraitAttribute("FeatureTitle", "BMI Calculator")] + [Xunit.TraitAttribute("Description", "BMI borderline between Normal and Overweight")] + public void BMIBorderlineBetweenNormalAndOverweight() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("BMI borderline between Normal and Overweight", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 19 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 20 + testRunner.Given("a height of 170 cm", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden +#line 21 + testRunner.And("a weight of 72 kg", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 22 + testRunner.When("I calculate BMI", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 23 + testRunner.Then("the BMI category should be \"Normal\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class FixtureData : System.IDisposable + { + + public FixtureData() + { + BMICalculatorFeature.FeatureSetup(); + } + + void System.IDisposable.Dispose() + { + BMICalculatorFeature.FeatureTearDown(); + } + } + } +} +#pragma warning restore +#endregion diff --git a/BPCalculator.BddTests/BMICalculatorSteps.cs b/BPCalculator.BddTests/BMICalculatorSteps.cs new file mode 100644 index 0000000..1b4b1d0 --- /dev/null +++ b/BPCalculator.BddTests/BMICalculatorSteps.cs @@ -0,0 +1,43 @@ +using BPCalculator; // to use BMICalculator +using FluentAssertions; +using TechTalk.SpecFlow; + +namespace BPCalculator.BddTests.StepDefinitions +{ + [Binding] + public class BMICalculatorSteps + { + private double _heightCm; + private double _weightKg; + private (double bmi, string category) _result; + + // Matches: "Given a height of cm" + [Given(@"a height of (.*) cm")] + public void GivenAHeightOfCm(double heightCm) + { + _heightCm = heightCm; + } + + // Matches: "And a weight of kg" + [Given(@"a weight of (.*) kg")] + public void GivenAWeightOfKg(double weightKg) + { + _weightKg = weightKg; + } + + // Matches: "When I calculate BMI" + [When(@"I calculate BMI")] + public void WhenICalculateBMI() + { + _result = BMICalculator.Calculate(_heightCm, _weightKg); + } + + // Matches: Then the BMI category should be "" + [Then(@"the BMI category should be ""(.*)""")] + public void ThenTheBMICategoryShouldBe(string expectedCategory) + { + _result.category.Should().Be(expectedCategory); + _result.bmi.Should().BeGreaterThan(0); + } + } +} diff --git a/BPCalculator.PlaywrightTests/BPCalculator.PlaywrightTests.csproj b/BPCalculator.PlaywrightTests/BPCalculator.PlaywrightTests.csproj new file mode 100644 index 0000000..1c49240 --- /dev/null +++ b/BPCalculator.PlaywrightTests/BPCalculator.PlaywrightTests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/BPCalculator.PlaywrightTests/BmiPagePlaywrightTests.cs b/BPCalculator.PlaywrightTests/BmiPagePlaywrightTests.cs new file mode 100644 index 0000000..8836f3b --- /dev/null +++ b/BPCalculator.PlaywrightTests/BmiPagePlaywrightTests.cs @@ -0,0 +1,68 @@ +using System.Threading.Tasks; +using Microsoft.Playwright; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +// Aliases to avoid any Assert name clashes +using MsAssert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using MsStringAssert = Microsoft.VisualStudio.TestTools.UnitTesting.StringAssert; + +namespace BPCalculator.PlaywrightTests +{ + [TestClass] + public class BmiPagePlaywrightTests + { + private IPlaywright _playwright = null!; + private IBrowser _browser = null!; + private IPage _page = null!; + + [TestInitialize] + public async Task SetUp() + { + _playwright = await Playwright.CreateAsync(); + _browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions + { + Headless = true + }); + _page = await _browser.NewPageAsync(); + } + + [TestCleanup] + public async Task TearDown() + { + await _browser.CloseAsync(); + _playwright.Dispose(); + } + + [TestMethod] + public async Task BmiResult_IsCalculatedAndVisible() + { + // Arrange + await _page.GotoAsync("http://localhost:5000/"); + + // Fill BP (values don’t matter for BMI) + await _page.FillAsync("input[name='BP.Systolic']", "120"); + await _page.FillAsync("input[name='BP.Diastolic']", "80"); + + // Act – fill BMI fields + await _page.FillAsync("input[name='HeightCm']", "170"); + await _page.FillAsync("input[name='WeightKg']", "65"); + + // Click submit + await _page.ClickAsync("input[type='submit']"); + + // Assert – BMI result box should be visible + var bmiResult = _page.Locator("#bmiResult"); + await Assertions.Expect(bmiResult).ToBeVisibleAsync(); + + var text = await bmiResult.InnerTextAsync(); + + // text should not be empty + MsAssert.IsFalse(string.IsNullOrWhiteSpace(text), $"BMI result text was: '{text}'"); + + // and should include a category in parentheses, e.g. "(Normal)" + MsStringAssert.Contains(text, "(", $"BMI result text was: '{text}'"); + + + } + } +} diff --git a/BPCalculator.PlaywrightTests/UnitTest1.cs b/BPCalculator.PlaywrightTests/UnitTest1.cs new file mode 100644 index 0000000..0cdcf88 --- /dev/null +++ b/BPCalculator.PlaywrightTests/UnitTest1.cs @@ -0,0 +1,11 @@ +namespace BPCalculator.PlaywrightTests +{ + public class UnitTest1 + { + [Fact] + public void Test1() + { + + } + } +} \ No newline at end of file diff --git a/BPCalculator.SeleniumTests/BPCalculator.SeleniumTests.vbproj b/BPCalculator.SeleniumTests/BPCalculator.SeleniumTests.vbproj new file mode 100644 index 0000000..7e3c1df --- /dev/null +++ b/BPCalculator.SeleniumTests/BPCalculator.SeleniumTests.vbproj @@ -0,0 +1,27 @@ + + + + BPCalculator.SeleniumTests + net8.0 + + false + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/BPCalculator.SeleniumTests/UnitTest1.vb b/BPCalculator.SeleniumTests/UnitTest1.vb new file mode 100644 index 0000000..33f12e3 --- /dev/null +++ b/BPCalculator.SeleniumTests/UnitTest1.vb @@ -0,0 +1,63 @@ +Imports System +Imports Microsoft.VisualStudio.TestTools.UnitTesting +Imports OpenQA.Selenium +Imports OpenQA.Selenium.Chrome + +' Alias to avoid Assert ambiguity if xUnit is referenced anywhere +Imports MsAssert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert + +Namespace BPCalculator.SeleniumTests + + + Public Class BloodPressureSeleniumTests + + Private _driver As IWebDriver + + + Public Sub SetUp() + Dim options As New ChromeOptions() + options.AddArgument("--headless=new") ' run headless for CI + options.AddArgument("--no-sandbox") + options.AddArgument("--disable-dev-shm-usage") + + _driver = New ChromeDriver(options) + End Sub + + + Public Sub TearDown() + If _driver IsNot Nothing Then + _driver.Quit() + End If + End Sub + + + Public Sub BpCalculator_Displays_Ideal_For_100_60() + ' NOTE: Make sure the BPCalculator web app is running at http://localhost:5000 + _driver.Navigate().GoToUrl("http://localhost:5000/") + + ' Fill in BP values + Dim systolic = _driver.FindElement(By.Name("BP.Systolic")) + systolic.Clear() + systolic.SendKeys("100") + + Dim diastolic = _driver.FindElement(By.Name("BP.Diastolic")) + diastolic.Clear() + diastolic.SendKeys("60") + + ' Submit the form + Dim submit = _driver.FindElement(By.CssSelector("input[type='submit']")) + submit.Click() + + ' Simple wait for the result to render + _driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(2) + + ' Read the BP result + Dim bpResultDiv = _driver.FindElement(By.Id("bpResult")) + Dim text = bpResultDiv.Text + + MsAssert.AreEqual("Ideal", text, $"Expected BP category 'Ideal' but got '{text}'") + End Sub + + End Class + +End Namespace diff --git a/BPCalculator.UnitTests/BMICalculatorTests.cs b/BPCalculator.UnitTests/BMICalculatorTests.cs new file mode 100644 index 0000000..4af5167 --- /dev/null +++ b/BPCalculator.UnitTests/BMICalculatorTests.cs @@ -0,0 +1,51 @@ +using Xunit; + +namespace BPCalculator.UnitTests +{ + public class BMICalculatorTests + { + // Height is constant so the weight decides category + [Theory(DisplayName = "BMI: category scenarios")] + [InlineData(170, 50, "Underweight")] + [InlineData(170, 65, "Normal")] + [InlineData(170, 80, "Overweight")] + [InlineData(170, 100, "Obese")] + public void Calculate_ReturnsExpectedCategory( + double heightCm, + double weightKg, + string expectedCategory) + { + // Act + var (bmi, category) = BMICalculator.Calculate(heightCm, weightKg); + + // Assert + Assert.True(bmi > 0); + Assert.Equal(expectedCategory, category); + } + + [Fact] + public void Calculate_RoundsToOneDecimalPlace() + { + // Act + var (bmi, _) = BMICalculator.Calculate(180, 80); // ~24.69 + + // Assert – should be rounded to 1 decimal (24.7) + Assert.Equal(24.7, bmi); + } + + [Fact] + public void Calculate_HeavierWeightProducesHigherBMI() + { + // Arrange + double height = 170; + + // Act + var (bmiLighter, _) = BMICalculator.Calculate(height, 65); + var (bmiHeavier, _) = BMICalculator.Calculate(height, 80); + + // Assert + Assert.True(bmiHeavier > bmiLighter); + } + + } +} diff --git a/BPCalculator.sln b/BPCalculator.sln index 38be67b..cd9623d 100644 --- a/BPCalculator.sln +++ b/BPCalculator.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.36518.9 d17.14 +VisualStudioVersion = 17.14.36518.9 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BPCalculator", "BPCalculator\BPCalculator.csproj", "{5966DA6C-819D-4303-ADC7-425B8541EC84}" EndProject @@ -14,8 +14,13 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{478B5C70-9FEE-464F-B914-E80DA228D2D7}" ProjectSection(SolutionItems) = preProject ci.yml = ci.yml + sonar-ci.yml = sonar-ci.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BPCalculator.PlaywrightTests", "BPCalculator.PlaywrightTests\BPCalculator.PlaywrightTests.csproj", "{D03C58EC-21EC-420A-9FEE-394C32D896AA}" +EndProject +Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "BPCalculator.SeleniumTests", "BPCalculator.SeleniumTests\BPCalculator.SeleniumTests.vbproj", "{E305B2F5-9758-4AF8-927E-0CA366CDA494}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -62,6 +67,30 @@ Global {59819ED5-D778-4737-8D70-9A61494224ED}.Release|x64.Build.0 = Release|Any CPU {59819ED5-D778-4737-8D70-9A61494224ED}.Release|x86.ActiveCfg = Release|Any CPU {59819ED5-D778-4737-8D70-9A61494224ED}.Release|x86.Build.0 = Release|Any CPU + {D03C58EC-21EC-420A-9FEE-394C32D896AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D03C58EC-21EC-420A-9FEE-394C32D896AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D03C58EC-21EC-420A-9FEE-394C32D896AA}.Debug|x64.ActiveCfg = Debug|Any CPU + {D03C58EC-21EC-420A-9FEE-394C32D896AA}.Debug|x64.Build.0 = Debug|Any CPU + {D03C58EC-21EC-420A-9FEE-394C32D896AA}.Debug|x86.ActiveCfg = Debug|Any CPU + {D03C58EC-21EC-420A-9FEE-394C32D896AA}.Debug|x86.Build.0 = Debug|Any CPU + {D03C58EC-21EC-420A-9FEE-394C32D896AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D03C58EC-21EC-420A-9FEE-394C32D896AA}.Release|Any CPU.Build.0 = Release|Any CPU + {D03C58EC-21EC-420A-9FEE-394C32D896AA}.Release|x64.ActiveCfg = Release|Any CPU + {D03C58EC-21EC-420A-9FEE-394C32D896AA}.Release|x64.Build.0 = Release|Any CPU + {D03C58EC-21EC-420A-9FEE-394C32D896AA}.Release|x86.ActiveCfg = Release|Any CPU + {D03C58EC-21EC-420A-9FEE-394C32D896AA}.Release|x86.Build.0 = Release|Any CPU + {E305B2F5-9758-4AF8-927E-0CA366CDA494}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E305B2F5-9758-4AF8-927E-0CA366CDA494}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E305B2F5-9758-4AF8-927E-0CA366CDA494}.Debug|x64.ActiveCfg = Debug|Any CPU + {E305B2F5-9758-4AF8-927E-0CA366CDA494}.Debug|x64.Build.0 = Debug|Any CPU + {E305B2F5-9758-4AF8-927E-0CA366CDA494}.Debug|x86.ActiveCfg = Debug|Any CPU + {E305B2F5-9758-4AF8-927E-0CA366CDA494}.Debug|x86.Build.0 = Debug|Any CPU + {E305B2F5-9758-4AF8-927E-0CA366CDA494}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E305B2F5-9758-4AF8-927E-0CA366CDA494}.Release|Any CPU.Build.0 = Release|Any CPU + {E305B2F5-9758-4AF8-927E-0CA366CDA494}.Release|x64.ActiveCfg = Release|Any CPU + {E305B2F5-9758-4AF8-927E-0CA366CDA494}.Release|x64.Build.0 = Release|Any CPU + {E305B2F5-9758-4AF8-927E-0CA366CDA494}.Release|x86.ActiveCfg = Release|Any CPU + {E305B2F5-9758-4AF8-927E-0CA366CDA494}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BPCalculator/BMICalculator.cs b/BPCalculator/BMICalculator.cs new file mode 100644 index 0000000..7e0fc55 --- /dev/null +++ b/BPCalculator/BMICalculator.cs @@ -0,0 +1,21 @@ +namespace BPCalculator +{ + public static class BMICalculator + { + // New feature: BMI calculation + category + public static (double bmi, string category) Calculate(double heightCm, double weightKg) + { + var heightM = heightCm / 100.0; + var bmi = System.Math.Round(weightKg / (heightM * heightM), 1); + + string category = + bmi < 18.5 ? "Underweight" : + bmi < 25.0 ? "Normal" : + bmi < 30.0 ? "Overweight" : + "Obese"; + + return (bmi, category); + } + } +} + diff --git a/BPCalculator/Pages/Index.cshtml b/BPCalculator/Pages/Index.cshtml index ab44c3c..ca859bf 100644 --- a/BPCalculator/Pages/Index.cshtml +++ b/BPCalculator/Pages/Index.cshtml @@ -1,11 +1,14 @@ @page @model BPCalculator.Pages.BloodPressureModel + @{ ViewData["Title"] = "BP Category Calculator"; -} -@{ + string resultClass = ""; + // default BMI result style (neutral) + string bmiResultClass = "alert alert-secondary"; + // BP result colour if (ViewData.ModelState.IsValid && Model.BP != null) { switch (Model.BP.Category) @@ -27,51 +30,90 @@ break; } } -} + // BMI result colour + if (Model.BMI.HasValue) + { + bmiResultClass = Model.BMICategory == "Normal" + ? "alert alert-success" + : "alert alert-warning"; + } +}

BP Category Calculator


-
+
-
+
+ + + +
-
- - - -
+
+ + + +
-
- - - -
+ +
BMI Calculator
-
- -
+
+ + + +
+ +
+ + + +
+
+ +
- @if (ViewData.ModelState.IsValid && Model.BP !=null) + @* BP result *@ + @if (ViewData.ModelState.IsValid && Model.BP != null) { -
- -
- @Model.BP.Category -
+
+ +
+ @Model.BP.Category +
} + + @* BMI result (always rendered) *@ +
+ +
+ @if (Model.BMI.HasValue) + { + @($"{Model.BMI} ({Model.BMICategory})") + } + else + { + No BMI calculated yet. + } +
+
+
- -
+
+ +@section Scripts { + +} diff --git a/BPCalculator/Pages/Index.cshtml.cs b/BPCalculator/Pages/Index.cshtml.cs index f96dc16..89885d6 100644 --- a/BPCalculator/Pages/Index.cshtml.cs +++ b/BPCalculator/Pages/Index.cshtml.cs @@ -7,9 +7,20 @@ namespace BPCalculator.Pages { public class BloodPressureModel : PageModel { - [BindProperty] // bound on POST + [BindProperty] // bound on POST public BloodPressure BP { get; set; } + // ---- BMI NEW FEATURE PROPERTIES ---- + [BindProperty] + public double? HeightCm { get; set; } + + [BindProperty] + public double? WeightKg { get; set; } + + public double? BMI { get; private set; } + + public string BMICategory { get; private set; } + // setup initial data public void OnGet() { @@ -19,12 +30,30 @@ public void OnGet() // POST, validate public IActionResult OnPost() { - // extra validation + // extra validation for blood pressure if (!(BP.Systolic > BP.Diastolic)) { ModelState.AddModelError("", "Systolic must be greater than Diastolic"); } + + // ---- BMI new feature logic ---- + if (HeightCm.HasValue && HeightCm > 0 && + WeightKg.HasValue && WeightKg > 0) + { + var (bmi, category) = BMICalculator.Calculate( + HeightCm.Value, + WeightKg.Value); + + BMI = bmi; + BMICategory = category; + } + else + { + BMI = null; + BMICategory = null; + } + return Page(); } } -} \ No newline at end of file +} diff --git a/ci.yml b/ci.yml index e69de29..318227f 100644 --- a/ci.yml +++ b/ci.yml @@ -0,0 +1,69 @@ +name: .NET CI with SonarCloud + +on: + push: + branches: + - main + - feature/** + pull_request: + branches: + - main + +jobs: + build-test-sonar: + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: write + + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_ORG: ${{ secrets.SONAR_ORG }} + SONAR_PROJECT_KEY: ${{ secrets.SONAR_PROJECT_KEY }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # SonarCloud prefers full history + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Install SonarScanner for .NET + run: dotnet tool install --global dotnet-sonarscanner + + - name: Add .NET tools to PATH + run: echo "$HOME/.dotnet/tools" >> $GITHUB_PATH + + # ---- SonarCloud begin ---- + - name: SonarCloud - Begin analysis + run: > + dotnet-sonarscanner begin + /k:"$SONAR_PROJECT_KEY" + /o:"$SONAR_ORG" + /d:sonar.host.url="https://sonarcloud.io" + /d:sonar.token="$SONAR_TOKEN" + /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" + # -------------------------- + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release + + - name: Test with coverage + run: > + dotnet test --no-build --configuration Release + /p:CollectCoverage=true + /p:CoverletOutput=./TestResults/ + /p:CoverletOutputFormat=opencover + + # ---- SonarCloud end ---- + - name: SonarCloud - End analysis + run: dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN" + # ------------------------ diff --git a/k6/bp_performance_test.js b/k6/bp_performance_test.js new file mode 100644 index 0000000..bc0e455 --- /dev/null +++ b/k6/bp_performance_test.js @@ -0,0 +1,24 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + stages: [ + { duration: '10s', target: 10 }, // ramp to 10 VUs + { duration: '10s', target: 20 }, // ramp to 20 VUs + { duration: '10s', target: 0 }, // ramp down + ], + thresholds: { + http_req_duration: ['p(95)<500'], // 95% under 500ms + http_req_failed: ['rate<0.01'], // <1% errors + }, +}; + +export default function () { + const res = http.get('http://localhost:5000/'); + + check(res, { + 'status is 200': (r) => r.status === 200, + }); + + sleep(1); +} diff --git a/sonar-ci.yml b/sonar-ci.yml new file mode 100644 index 0000000..318227f --- /dev/null +++ b/sonar-ci.yml @@ -0,0 +1,69 @@ +name: .NET CI with SonarCloud + +on: + push: + branches: + - main + - feature/** + pull_request: + branches: + - main + +jobs: + build-test-sonar: + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: write + + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_ORG: ${{ secrets.SONAR_ORG }} + SONAR_PROJECT_KEY: ${{ secrets.SONAR_PROJECT_KEY }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # SonarCloud prefers full history + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Install SonarScanner for .NET + run: dotnet tool install --global dotnet-sonarscanner + + - name: Add .NET tools to PATH + run: echo "$HOME/.dotnet/tools" >> $GITHUB_PATH + + # ---- SonarCloud begin ---- + - name: SonarCloud - Begin analysis + run: > + dotnet-sonarscanner begin + /k:"$SONAR_PROJECT_KEY" + /o:"$SONAR_ORG" + /d:sonar.host.url="https://sonarcloud.io" + /d:sonar.token="$SONAR_TOKEN" + /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" + # -------------------------- + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release + + - name: Test with coverage + run: > + dotnet test --no-build --configuration Release + /p:CollectCoverage=true + /p:CoverletOutput=./TestResults/ + /p:CoverletOutputFormat=opencover + + # ---- SonarCloud end ---- + - name: SonarCloud - End analysis + run: dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN" + # ------------------------ From 564b413ca29b5a803e298b03f02df52f8731dd39 Mon Sep 17 00:00:00 2001 From: kaycan26 Date: Fri, 21 Nov 2025 16:14:54 +0000 Subject: [PATCH 3/6] Limit Sonar tests to UniTests --- sonar-ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sonar-ci.yml b/sonar-ci.yml index 318227f..f22411b 100644 --- a/sonar-ci.yml +++ b/sonar-ci.yml @@ -56,9 +56,10 @@ jobs: - name: Build run: dotnet build --no-restore --configuration Release - - name: Test with coverage + - name: Test UnitTests with coverage run: > - dotnet test --no-build --configuration Release + dotnet test ./BPCalculator.UnitTests/BPCalculator.UnitTests.csproj + --no-build --configuration Release /p:CollectCoverage=true /p:CoverletOutput=./TestResults/ /p:CoverletOutputFormat=opencover From c55d1226183f14174a2bfbf2083f3b10e0b17337 Mon Sep 17 00:00:00 2001 From: kaycan26 Date: Fri, 21 Nov 2025 23:54:15 +0000 Subject: [PATCH 4/6] Update Selenium tests + CI workflow improvements Fixed Selenium wait issues, updated GitHub Actions YAML files, and improved SonarCloud integration. --- BPCalculator.SeleniumTests/UnitTest1.vb | 35 +++++++++++++++++++------ ci.yml | 26 ++++++++++++++++-- sonar-ci.yml | 19 +++++++------- 3 files changed, 61 insertions(+), 19 deletions(-) diff --git a/BPCalculator.SeleniumTests/UnitTest1.vb b/BPCalculator.SeleniumTests/UnitTest1.vb index 33f12e3..c30f643 100644 --- a/BPCalculator.SeleniumTests/UnitTest1.vb +++ b/BPCalculator.SeleniumTests/UnitTest1.vb @@ -3,7 +3,7 @@ Imports Microsoft.VisualStudio.TestTools.UnitTesting Imports OpenQA.Selenium Imports OpenQA.Selenium.Chrome -' Alias to avoid Assert ambiguity if xUnit is referenced anywhere +' Alias to avoid Assert ambiguity if xUnit is ever referenced Imports MsAssert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert Namespace BPCalculator.SeleniumTests @@ -16,11 +16,14 @@ Namespace BPCalculator.SeleniumTests Public Sub SetUp() Dim options As New ChromeOptions() - options.AddArgument("--headless=new") ' run headless for CI + options.AddArgument("--headless=new") ' run headless for CI options.AddArgument("--no-sandbox") options.AddArgument("--disable-dev-shm-usage") _driver = New ChromeDriver(options) + + ' Simple implicit wait to give the page time to render elements + _driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5) End Sub @@ -48,16 +51,32 @@ Namespace BPCalculator.SeleniumTests Dim submit = _driver.FindElement(By.CssSelector("input[type='submit']")) submit.Click() - ' Simple wait for the result to render - _driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(2) - - ' Read the BP result - Dim bpResultDiv = _driver.FindElement(By.Id("bpResult")) - Dim text = bpResultDiv.Text + ' Read the BP result with a small retry loop to avoid stale-element issues + Dim text As String = GetResultTextWithRetry() MsAssert.AreEqual("Ideal", text, $"Expected BP category 'Ideal' but got '{text}'") End Sub + ' Helper to handle StaleElementReferenceException if the result div is re-rendered + Private Function GetResultTextWithRetry() As String + Dim attempts As Integer = 0 + + While True + Try + Dim bpResultDiv = _driver.FindElement(By.Id("bpResult")) + Return bpResultDiv.Text + Catch ex As StaleElementReferenceException + attempts += 1 + If attempts >= 3 Then + Throw + End If + + ' wait a tiny bit before trying again + Threading.Thread.Sleep(500) + End Try + End While + End Function + End Class End Namespace diff --git a/ci.yml b/ci.yml index 318227f..0744170 100644 --- a/ci.yml +++ b/ci.yml @@ -56,13 +56,35 @@ jobs: - name: Build run: dotnet build --no-restore --configuration Release - - name: Test with coverage + # ---- Tests ---- + # Unit tests with coverage (for SonarCloud) + - name: Unit tests with coverage run: > - dotnet test --no-build --configuration Release + dotnet test BPCalculator.UnitTests/BPCalculator.UnitTests.csproj + --no-build --configuration Release /p:CollectCoverage=true /p:CoverletOutput=./TestResults/ /p:CoverletOutputFormat=opencover + # BDD tests + - name: BDD tests + run: > + dotnet test BPCalculator.BddTests/BPCalculator.BddTests.csproj + --no-build --configuration Release + + # Playwright UI tests + - name: Playwright tests + run: > + dotnet test BPCalculator.PlaywrightTests/BPCalculator.PlaywrightTests.csproj + --no-build --configuration Release + + # Selenium UI tests + - name: Selenium tests + run: > + dotnet test BPCalculator.SeleniumTests/BPCalculator.SeleniumTests.csproj + --no-build --configuration Release + # ---- End tests ---- + # ---- SonarCloud end ---- - name: SonarCloud - End analysis run: dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN" diff --git a/sonar-ci.yml b/sonar-ci.yml index f22411b..ebf4c35 100644 --- a/sonar-ci.yml +++ b/sonar-ci.yml @@ -1,16 +1,16 @@ -name: .NET CI with SonarCloud +name: SonarCloud analysis (.NET) on: push: branches: - - main - - feature/** + - main # run when main is updated pull_request: branches: - - main + - main # run on PRs targeting main + workflow_dispatch: # allow manual runs from the Actions tab jobs: - build-test-sonar: + sonar-analysis: runs-on: ubuntu-latest permissions: @@ -26,7 +26,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 0 # SonarCloud prefers full history + fetch-depth: 0 # SonarCloud prefers full history for blame info - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -37,7 +37,7 @@ jobs: run: dotnet tool install --global dotnet-sonarscanner - name: Add .NET tools to PATH - run: echo "$HOME/.dotnet/tools" >> $GITHUB_PATH + run: echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH" # ---- SonarCloud begin ---- - name: SonarCloud - Begin analysis @@ -47,7 +47,7 @@ jobs: /o:"$SONAR_ORG" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.token="$SONAR_TOKEN" - /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" + /d:sonar.cs.opencover.reportsPaths="**/TestResults/coverage.opencover.xml" # -------------------------- - name: Restore @@ -56,12 +56,13 @@ jobs: - name: Build run: dotnet build --no-restore --configuration Release + # Only UnitTests for coverage (no Selenium / Playwright here) - name: Test UnitTests with coverage run: > dotnet test ./BPCalculator.UnitTests/BPCalculator.UnitTests.csproj --no-build --configuration Release /p:CollectCoverage=true - /p:CoverletOutput=./TestResults/ + /p:CoverletOutput=./TestResults/coverage /p:CoverletOutputFormat=opencover # ---- SonarCloud end ---- From 171bad31cbf6321a497644dd822f3d2488d4d7d5 Mon Sep 17 00:00:00 2001 From: kaycan26 Date: Sat, 22 Nov 2025 21:24:47 +0000 Subject: [PATCH 5/6] Add Playwright & Selenium jobs) --- .../BPCalculator.PerformanceTests.csproj | 9 ++ BPCalculator.PerformanceTests/Class1.cs | 7 + .../performance/bp_bmi_smoke_test.js | 20 +++ .../BmiPagePlaywrightTests.cs | 78 ++++++---- BPCalculator.SeleniumTests/UnitTest1.vb | 147 ++++++++---------- BPCalculator.sln | 14 ++ BPCalculator/BMICalculator.cs | 69 +++++++- BPCalculator/BloodPressure.cs | 117 ++++++++++++-- BPCalculator/Pages/Index.cshtml | 34 ++-- BPCalculator/Pages/Index.cshtml.cs | 19 ++- BPCalculator/wwwroot/css/site.css | 89 +++++------ ci.yml | 48 ++++-- 12 files changed, 457 insertions(+), 194 deletions(-) create mode 100644 BPCalculator.PerformanceTests/BPCalculator.PerformanceTests.csproj create mode 100644 BPCalculator.PerformanceTests/Class1.cs create mode 100644 BPCalculator.PerformanceTests/performance/bp_bmi_smoke_test.js diff --git a/BPCalculator.PerformanceTests/BPCalculator.PerformanceTests.csproj b/BPCalculator.PerformanceTests/BPCalculator.PerformanceTests.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/BPCalculator.PerformanceTests/BPCalculator.PerformanceTests.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/BPCalculator.PerformanceTests/Class1.cs b/BPCalculator.PerformanceTests/Class1.cs new file mode 100644 index 0000000..d954116 --- /dev/null +++ b/BPCalculator.PerformanceTests/Class1.cs @@ -0,0 +1,7 @@ +namespace BPCalculator.PerformanceTests +{ + public class Class1 + { + + } +} diff --git a/BPCalculator.PerformanceTests/performance/bp_bmi_smoke_test.js b/BPCalculator.PerformanceTests/performance/bp_bmi_smoke_test.js new file mode 100644 index 0000000..fbdae8e --- /dev/null +++ b/BPCalculator.PerformanceTests/performance/bp_bmi_smoke_test.js @@ -0,0 +1,20 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + vus: 5, // 5 virtual users + duration: '30s', // run for 30 seconds +}; + +export default function () { + // APP_BASE_URL is passed from GitHub Actions as an environment variable + const res = http.get(`${__ENV.APP_BASE_URL}/`); + + check(res, { + 'status is 200': (r) => r.status === 200, + 'page has BP form': (r) => r.body.includes('BP Category Calculator'), + 'page loads under 500ms': (r) => r.timings.duration < 500, + }); + + sleep(1); +} diff --git a/BPCalculator.PlaywrightTests/BmiPagePlaywrightTests.cs b/BPCalculator.PlaywrightTests/BmiPagePlaywrightTests.cs index 8836f3b..bec67be 100644 --- a/BPCalculator.PlaywrightTests/BmiPagePlaywrightTests.cs +++ b/BPCalculator.PlaywrightTests/BmiPagePlaywrightTests.cs @@ -1,68 +1,88 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Microsoft.Playwright; using Microsoft.VisualStudio.TestTools.UnitTesting; - -// Aliases to avoid any Assert name clashes -using MsAssert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; -using MsStringAssert = Microsoft.VisualStudio.TestTools.UnitTesting.StringAssert; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; namespace BPCalculator.PlaywrightTests { [TestClass] public class BmiPagePlaywrightTests { - private IPlaywright _playwright = null!; - private IBrowser _browser = null!; - private IPage _page = null!; + private IPlaywright? _playwright; + private IBrowser? _browser; + private IBrowserContext? _context; + private IPage? _page; + + // Base URL: APP_URL for pipeline, localhost for dev + private readonly string _baseUrl = + Environment.GetEnvironmentVariable("APP_URL") + ?? "http://localhost:5000"; [TestInitialize] public async Task SetUp() { _playwright = await Playwright.CreateAsync(); + _browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = true }); - _page = await _browser.NewPageAsync(); + + _context = await _browser.NewContextAsync(); + _page = await _context.NewPageAsync(); } [TestCleanup] public async Task TearDown() { - await _browser.CloseAsync(); - _playwright.Dispose(); + if (_page is not null) + await _page.CloseAsync(); + + if (_context is not null) + await _context.CloseAsync(); + + if (_browser is not null) + await _browser.CloseAsync(); + + _playwright?.Dispose(); } [TestMethod] public async Task BmiResult_IsCalculatedAndVisible() { - // Arrange - await _page.GotoAsync("http://localhost:5000/"); + Assert.IsNotNull(_page, "Playwright page was not initialised."); - // Fill BP (values don’t matter for BMI) - await _page.FillAsync("input[name='BP.Systolic']", "120"); - await _page.FillAsync("input[name='BP.Diastolic']", "80"); + // Go to the home page of the app + await _page!.GotoAsync(_baseUrl); - // Act – fill BMI fields - await _page.FillAsync("input[name='HeightCm']", "170"); - await _page.FillAsync("input[name='WeightKg']", "65"); + // Fill BMI inputs (height + weight) + await _page.FillAsync("#HeightCm", "170"); + await _page.FillAsync("#WeightKg", "65"); - // Click submit + // Click Submit await _page.ClickAsync("input[type='submit']"); - // Assert – BMI result box should be visible + // Wait for BMI result panel var bmiResult = _page.Locator("#bmiResult"); - await Assertions.Expect(bmiResult).ToBeVisibleAsync(); - - var text = await bmiResult.InnerTextAsync(); + await bmiResult.WaitForAsync(); - // text should not be empty - MsAssert.IsFalse(string.IsNullOrWhiteSpace(text), $"BMI result text was: '{text}'"); - - // and should include a category in parentheses, e.g. "(Normal)" - MsStringAssert.Contains(text, "(", $"BMI result text was: '{text}'"); + // Assert: panel is visible + Assert.IsTrue( + await bmiResult.IsVisibleAsync(), + "BMI result panel is not visible." + ); + var text = await bmiResult.InnerTextAsync(); + // Assert: BMI result contains one of the known categories + Assert.IsTrue( + text.Contains("Normal", StringComparison.OrdinalIgnoreCase) || + text.Contains("Underweight", StringComparison.OrdinalIgnoreCase) || + text.Contains("Overweight", StringComparison.OrdinalIgnoreCase) || + text.Contains("Obese", StringComparison.OrdinalIgnoreCase), + $"Unexpected BMI result text: '{text}'" + ); } } } diff --git a/BPCalculator.SeleniumTests/UnitTest1.vb b/BPCalculator.SeleniumTests/UnitTest1.vb index c30f643..45806b1 100644 --- a/BPCalculator.SeleniumTests/UnitTest1.vb +++ b/BPCalculator.SeleniumTests/UnitTest1.vb @@ -1,82 +1,71 @@ -Imports System Imports Microsoft.VisualStudio.TestTools.UnitTesting Imports OpenQA.Selenium Imports OpenQA.Selenium.Chrome - -' Alias to avoid Assert ambiguity if xUnit is ever referenced -Imports MsAssert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert - -Namespace BPCalculator.SeleniumTests - - - Public Class BloodPressureSeleniumTests - - Private _driver As IWebDriver - - - Public Sub SetUp() - Dim options As New ChromeOptions() - options.AddArgument("--headless=new") ' run headless for CI - options.AddArgument("--no-sandbox") - options.AddArgument("--disable-dev-shm-usage") - - _driver = New ChromeDriver(options) - - ' Simple implicit wait to give the page time to render elements - _driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5) - End Sub - - - Public Sub TearDown() - If _driver IsNot Nothing Then - _driver.Quit() - End If - End Sub - - - Public Sub BpCalculator_Displays_Ideal_For_100_60() - ' NOTE: Make sure the BPCalculator web app is running at http://localhost:5000 - _driver.Navigate().GoToUrl("http://localhost:5000/") - - ' Fill in BP values - Dim systolic = _driver.FindElement(By.Name("BP.Systolic")) - systolic.Clear() - systolic.SendKeys("100") - - Dim diastolic = _driver.FindElement(By.Name("BP.Diastolic")) - diastolic.Clear() - diastolic.SendKeys("60") - - ' Submit the form - Dim submit = _driver.FindElement(By.CssSelector("input[type='submit']")) - submit.Click() - - ' Read the BP result with a small retry loop to avoid stale-element issues - Dim text As String = GetResultTextWithRetry() - - MsAssert.AreEqual("Ideal", text, $"Expected BP category 'Ideal' but got '{text}'") - End Sub - - ' Helper to handle StaleElementReferenceException if the result div is re-rendered - Private Function GetResultTextWithRetry() As String - Dim attempts As Integer = 0 - - While True - Try - Dim bpResultDiv = _driver.FindElement(By.Id("bpResult")) - Return bpResultDiv.Text - Catch ex As StaleElementReferenceException - attempts += 1 - If attempts >= 3 Then - Throw - End If - - ' wait a tiny bit before trying again - Threading.Thread.Sleep(500) - End Try - End While - End Function - - End Class - -End Namespace +Imports OpenQA.Selenium.Support.UI + + +Public Class BloodPressureSeleniumTests + + Private driver As IWebDriver + Private wait As WebDriverWait + Private baseUrl As String = "http://localhost:5000" + + + Public Sub SetUp() + Dim options As New ChromeOptions() + options.AddArgument("--headless=new") + options.AddArgument("--no-sandbox") + options.AddArgument("--disable-gpu") + + driver = New ChromeDriver(options) + driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5) + wait = New WebDriverWait(driver, TimeSpan.FromSeconds(10)) + End Sub + + + Public Sub TearDown() + If driver IsNot Nothing Then + driver.Quit() + End If + End Sub + + + Public Sub BpCalculator_Displays_Ideal_For_100_60() + + driver.Navigate().GoToUrl(baseUrl) + + ' Fill systolic + Dim systolic = driver.FindElement(By.Id("BP_Systolic")) + systolic.Clear() + systolic.SendKeys("100") + + ' Fill diastolic + Dim diastolic = driver.FindElement(By.Id("BP_Diastolic")) + diastolic.Clear() + diastolic.SendKeys("60") + + ' Submit + Dim submitButton = driver.FindElement(By.CssSelector("input[type='submit']")) + submitButton.Click() + + ' Wait for the BP result panel + Dim bpResultDiv = wait.Until(Function(d) + Try + Dim el = d.FindElement(By.Id("bpResult")) + Return If(el.Displayed, el, Nothing) + Catch + Return Nothing + End Try + End Function) + + Assert.IsNotNull(bpResultDiv, "BP result div not found.") + + Dim resultText As String = bpResultDiv.Text + + ' Expected: "Ideal" + Assert.IsTrue(resultText.Contains("Ideal"), + $"Expected BP category 'Ideal' but got '{resultText}'") + + End Sub + +End Class diff --git a/BPCalculator.sln b/BPCalculator.sln index cd9623d..b024e90 100644 --- a/BPCalculator.sln +++ b/BPCalculator.sln @@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BPCalculator.PlaywrightTest EndProject Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "BPCalculator.SeleniumTests", "BPCalculator.SeleniumTests\BPCalculator.SeleniumTests.vbproj", "{E305B2F5-9758-4AF8-927E-0CA366CDA494}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BPCalculator.PerformanceTests", "BPCalculator.PerformanceTests\BPCalculator.PerformanceTests.csproj", "{AA0BD07F-4F77-4C98-859C-C46DCDD8438A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -91,6 +93,18 @@ Global {E305B2F5-9758-4AF8-927E-0CA366CDA494}.Release|x64.Build.0 = Release|Any CPU {E305B2F5-9758-4AF8-927E-0CA366CDA494}.Release|x86.ActiveCfg = Release|Any CPU {E305B2F5-9758-4AF8-927E-0CA366CDA494}.Release|x86.Build.0 = Release|Any CPU + {AA0BD07F-4F77-4C98-859C-C46DCDD8438A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA0BD07F-4F77-4C98-859C-C46DCDD8438A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA0BD07F-4F77-4C98-859C-C46DCDD8438A}.Debug|x64.ActiveCfg = Debug|Any CPU + {AA0BD07F-4F77-4C98-859C-C46DCDD8438A}.Debug|x64.Build.0 = Debug|Any CPU + {AA0BD07F-4F77-4C98-859C-C46DCDD8438A}.Debug|x86.ActiveCfg = Debug|Any CPU + {AA0BD07F-4F77-4C98-859C-C46DCDD8438A}.Debug|x86.Build.0 = Debug|Any CPU + {AA0BD07F-4F77-4C98-859C-C46DCDD8438A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA0BD07F-4F77-4C98-859C-C46DCDD8438A}.Release|Any CPU.Build.0 = Release|Any CPU + {AA0BD07F-4F77-4C98-859C-C46DCDD8438A}.Release|x64.ActiveCfg = Release|Any CPU + {AA0BD07F-4F77-4C98-859C-C46DCDD8438A}.Release|x64.Build.0 = Release|Any CPU + {AA0BD07F-4F77-4C98-859C-C46DCDD8438A}.Release|x86.ActiveCfg = Release|Any CPU + {AA0BD07F-4F77-4C98-859C-C46DCDD8438A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BPCalculator/BMICalculator.cs b/BPCalculator/BMICalculator.cs index 7e0fc55..0f7eb34 100644 --- a/BPCalculator/BMICalculator.cs +++ b/BPCalculator/BMICalculator.cs @@ -2,9 +2,18 @@ { public static class BMICalculator { - // New feature: BMI calculation + category + /// + /// New feature: BMI calculation + category. + /// This keeps the original signature so existing tests still compile. + /// public static (double bmi, string category) Calculate(double heightCm, double weightKg) { + if (heightCm <= 0 || weightKg <= 0) + { + // Defensive check – callers can handle "Invalid" if needed. + return (0.0, "Invalid"); + } + var heightM = heightCm / 100.0; var bmi = System.Math.Round(weightKg / (heightM * heightM), 1); @@ -16,6 +25,62 @@ public static (double bmi, string category) Calculate(double heightCm, double we return (bmi, category); } + + /// + /// Returns a CSS class name for styling the BMI result panel. + /// Define the colours in site.css. + /// + public static string CategoryCssClass(string category) => + category switch + { + "Underweight" => "bmi-underweight", + "Normal" => "bmi-normal", + "Overweight" => "bmi-overweight", + "Obese" => "bmi-obese", + _ => "bmi-unknown" + }; + + /// + /// Returns advice text for a given BMI result. + /// This is used by the Razor view to show contextual guidance. + /// + public static string AdviceText(double bmi, string category) + { + // For invalid input, do not pretend we have a meaningful BMI. + if (category == "Invalid") + { + return "Please enter a positive height (cm) and weight (kg). " + + "This tool is for information only and does not replace medical advice."; + } + + return category switch + { + "Underweight" => +$@"Your BMI is {bmi} – Underweight. +This suggests you may weigh less than is typical for your height. In some cases this can be normal, but in others it may be linked to health or nutrition issues. + +Consider discussing your weight, diet and any symptoms with a healthcare professional.", + + "Normal" => +$@"Your BMI is {bmi} – Normal. +This is within the generally healthy range. Maintaining regular movement, a balanced diet and sufficient sleep can help keep your BMI and blood pressure in this range.", + + "Overweight" => +$@"Your BMI is {bmi} – Overweight. +Your weight is above the typical range for your height. Gradual lifestyle changes may help over time, such as increasing activity and adjusting food portions. + +Talking to a healthcare professional can give you personalised advice.", + + "Obese" => +$@"Your BMI is {bmi} – Obese. +This level is associated with an increased risk of health problems such as high blood pressure and type 2 diabetes. + +Please speak with a doctor or healthcare professional about safe and realistic options for weight management. Avoid extreme or crash diets without professional support.", + + _ => +$@"Your BMI is {bmi}. +This tool is for information only and does not replace medical advice." + }; + } } } - diff --git a/BPCalculator/BloodPressure.cs b/BPCalculator/BloodPressure.cs index ce9f8fa..68e1a5e 100644 --- a/BPCalculator/BloodPressure.cs +++ b/BPCalculator/BloodPressure.cs @@ -7,10 +7,10 @@ namespace BPCalculator // BP categories public enum BPCategory { - [Display(Name="Low Blood Pressure")] Low, - [Display(Name="Ideal Blood Pressure")] Ideal, - [Display(Name="Pre-High Blood Pressure")] PreHigh, - [Display(Name ="High Blood Pressure")] High + [Display(Name = "Low Blood Pressure")] Low, + [Display(Name = "Ideal Blood Pressure")] Ideal, + [Display(Name = "Pre-High Blood Pressure")] PreHigh, + [Display(Name = "High Blood Pressure")] High }; public class BloodPressure @@ -21,12 +21,12 @@ public class BloodPressure public const int DiastolicMax = 100; [Range(SystolicMin, SystolicMax, ErrorMessage = "Invalid Systolic Value")] - public int Systolic { get; set; } // mmHG + public int Systolic { get; set; } // mmHG [Range(DiastolicMin, DiastolicMax, ErrorMessage = "Invalid Diastolic Value")] - public int Diastolic { get; set; } // mmHG + public int Diastolic { get; set; } // mmHG - // calculate BP category + // Calculate BP category (existing behaviour kept exactly the same) public BPCategory Category { get @@ -36,7 +36,7 @@ public BPCategory Category { return BPCategory.High; } - + // Pre-high blood pressure if ((Systolic >= 120 && Systolic <= 139) || (Diastolic >= 80 && Diastolic <= 89)) @@ -44,15 +44,112 @@ public BPCategory Category return BPCategory.PreHigh; } - // ideal blood pressure + // Ideal blood pressure if (Systolic >= 90 && Systolic <= 119 && Diastolic >= 60 && Diastolic <= 79) { return BPCategory.Ideal; } - // otherwise, Low blood pressure + + // Otherwise, Low blood pressure return BPCategory.Low; } } + + /// + /// Optional helper: returns the Display(Name=...) value for the current category. + /// + public string CategoryDisplayName + { + get + { + var field = typeof(BPCategory).GetField(Category.ToString()); + var attr = (DisplayAttribute?)Attribute.GetCustomAttribute(field, typeof(DisplayAttribute)); + return attr?.Name ?? Category.ToString(); + } + } + + /// + /// CSS class to apply in the UI for colour-coding the result. + /// The actual colours are defined in site.css. + /// + public string CategoryCssClass => Category switch + { + BPCategory.Low => "bp-low", + BPCategory.Ideal => "bp-ideal", + BPCategory.PreHigh => "bp-prehigh", + BPCategory.High => "bp-high", + _ => "bp-unknown" + }; + + /// + /// Human-readable advice text shown under the blood pressure result. + /// This is UI-facing but lives here so that tests and views share the same text source. + /// + public string AdviceText + { + get + { + // We include the reading in the text to make it clearer for the user. + var readingLine = $"Your reading: {Systolic} / {Diastolic} mmHg"; + + return Category switch + { + BPCategory.Low => $@" +{readingLine} + +Your blood pressure is lower than the typical range. Some people have naturally low readings and feel well, but others may experience dizziness or fainting. + +Practical tips: +• Stand up slowly after sitting or lying down +• Drink enough water during the day +• Avoid skipping meals + +If you feel dizzy, faint, or unwell, please contact a doctor or healthcare professional.", + + BPCategory.Ideal => $@" +{readingLine} + +Your blood pressure is in the ideal range. Your current lifestyle is likely supporting your cardiovascular health. + +To help keep it in this range: +• Stay physically active (e.g. brisk walking, cycling, swimming) +• Eat a balanced diet with fruit, vegetables and whole grains +• Limit very salty or highly processed foods +• Avoid smoking and keep alcohol intake low + +Always discuss changes in exercise or diet with a healthcare professional.", + + BPCategory.PreHigh => $@" +{readingLine} + +Your blood pressure is slightly above the ideal range. This may increase the risk of developing high blood pressure in the future. + +Lifestyle changes that may help: +• Reduce salt intake +• Increase daily movement (walking, stairs, active hobbies) +• Maintain a healthy weight +• Limit alcohol and avoid smoking + +Discuss your readings with a doctor or nurse, especially if they are often in this range.", + + BPCategory.High => $@" +{readingLine} + +This reading is in the high blood pressure range. Persistent high blood pressure can increase the risk of heart disease and stroke. + +Do not ignore this reading: +• Take another reading after resting quietly for 5–10 minutes +• Avoid caffeine and smoking before measuring + +Please contact your doctor or healthcare professional to discuss your readings. Do not change any medication based on this calculator alone.", + + _ => $@" +{readingLine} + +Please enter values within the supported range. This tool is for information only and does not replace medical advice." + }; + } + } } } diff --git a/BPCalculator/Pages/Index.cshtml b/BPCalculator/Pages/Index.cshtml index ca859bf..422ab7c 100644 --- a/BPCalculator/Pages/Index.cshtml +++ b/BPCalculator/Pages/Index.cshtml @@ -1,14 +1,14 @@ @page @model BPCalculator.Pages.BloodPressureModel +@using BPCalculator @{ ViewData["Title"] = "BP Category Calculator"; - string resultClass = ""; - // default BMI result style (neutral) + // BP Bootstrap classes (required for Selenium test compatibility) + string resultClass = "alert alert-secondary"; string bmiResultClass = "alert alert-secondary"; - // BP result colour if (ViewData.ModelState.IsValid && Model.BP != null) { switch (Model.BP.Category) @@ -31,8 +31,7 @@ } } - // BMI result colour - if (Model.BMI.HasValue) + if (Model.BMI.HasValue && !string.IsNullOrEmpty(Model.BMICategory)) { bmiResultClass = Model.BMICategory == "Normal" ? "alert alert-success" @@ -63,7 +62,7 @@ - +
BMI Calculator
@@ -83,24 +82,37 @@
- @* BP result *@ + @if (ViewData.ModelState.IsValid && Model.BP != null) {
+ +
- @Model.BP.Category + @Model.BP.Category.ToString() +
+ + +
+ @Model.BP.CategoryDisplayName +
+ @Html.Raw(Model.BP.AdviceText.Replace("\n", "
"))
} - @* BMI result (always rendered) *@ +
+
- @if (Model.BMI.HasValue) + @if (Model.BMI.HasValue && !string.IsNullOrEmpty(Model.BMICategory)) { - @($"{Model.BMI} ({Model.BMICategory})") + + @($"{Model.BMI} ({Model.BMICategory})") +
+ @Html.Raw(Model.BMIAdvice.Replace("\n", "
")) } else { diff --git a/BPCalculator/Pages/Index.cshtml.cs b/BPCalculator/Pages/Index.cshtml.cs index 89885d6..ad69809 100644 --- a/BPCalculator/Pages/Index.cshtml.cs +++ b/BPCalculator/Pages/Index.cshtml.cs @@ -1,10 +1,9 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -// page model - namespace BPCalculator.Pages { + // page model public class BloodPressureModel : PageModel { [BindProperty] // bound on POST @@ -18,13 +17,21 @@ public class BloodPressureModel : PageModel public double? WeightKg { get; set; } public double? BMI { get; private set; } - public string BMICategory { get; private set; } + // NEW: for coloured BMI panel + advice text + public string BMICssClass { get; private set; } + public string BMIAdvice { get; private set; } + // setup initial data public void OnGet() { BP = new BloodPressure() { Systolic = 100, Diastolic = 60 }; + + BMI = null; + BMICategory = null; + BMICssClass = null; + BMIAdvice = null; } // POST, validate @@ -40,17 +47,23 @@ public IActionResult OnPost() if (HeightCm.HasValue && HeightCm > 0 && WeightKg.HasValue && WeightKg > 0) { + // use the updated BMICalculator helpers var (bmi, category) = BMICalculator.Calculate( HeightCm.Value, WeightKg.Value); BMI = bmi; BMICategory = category; + BMICssClass = BMICalculator.CategoryCssClass(category); + BMIAdvice = BMICalculator.AdviceText(bmi, category); } else { + // no valid BMI BMI = null; BMICategory = null; + BMICssClass = null; + BMIAdvice = null; } return Page(); diff --git a/BPCalculator/wwwroot/css/site.css b/BPCalculator/wwwroot/css/site.css index e679a8e..c556ae4 100644 --- a/BPCalculator/wwwroot/css/site.css +++ b/BPCalculator/wwwroot/css/site.css @@ -1,71 +1,58 @@ -/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification -for details on configuring this project to bundle and minify static web assets. */ +/* ---------- Result Panels (BP + BMI) ---------- */ -a.navbar-brand { - white-space: normal; - text-align: center; - word-break: break-all; +.bp-result-panel, +.bmi-result-panel { + padding: 1rem; + border-radius: 4px; + margin-top: 1rem; + margin-bottom: 1rem; + color: #222; + line-height: 1.4; } -/* Provide sufficient contrast against white background */ -a { - color: #0366d6; +/* BP COLOUR THEMES */ +.bp-low { + background-color: #b3e5fc !important; /* light blue */ + border-left: 5px solid #0277bd !important; /* blue */ } -.btn-primary { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; +.bp-ideal { + background-color: #c8f7d4 !important; /* light green */ + border-left: 5px solid #2e8b57 !important; /* dark green */ } -.nav-pills .nav-link.active, .nav-pills .show > .nav-link { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; +.bp-prehigh { + background-color: #ffe9bf !important; /* light orange */ + border-left: 5px solid #ff9800 !important; /* orange */ } -/* Sticky footer styles --------------------------------------------------- */ -html { - font-size: 14px; -} -@media (min-width: 768px) { - html { - font-size: 16px; - } +.bp-high { + background-color: #ffcdd2 !important; /* light red */ + border-left: 5px solid #c62828 !important; /* red */ } -.border-top { - border-top: 1px solid #e5e5e5; -} -.border-bottom { - border-bottom: 1px solid #e5e5e5; +/* BMI COLOUR THEMES */ +.bmi-underweight { + background-color: #b3e5fc !important; + border-left: 5px solid #0277bd !important; } -.box-shadow { - box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +.bmi-normal { + background-color: #c8f7d4 !important; + border-left: 5px solid #2e8b57 !important; } -button.accept-policy { - font-size: 1rem; - line-height: inherit; +.bmi-overweight { + background-color: #ffe9bf !important; + border-left: 5px solid #ff9800 !important; } -/* Sticky footer styles --------------------------------------------------- */ -html { - position: relative; - min-height: 100%; +.bmi-obese { + background-color: #ffcdd2 !important; + border-left: 5px solid #c62828 !important; } -body { - /* Margin bottom by footer height */ - margin-bottom: 60px; -} -.footer { - position: absolute; - bottom: 0; - width: 100%; - white-space: nowrap; - line-height: 60px; /* Vertically center the text there */ +.bmi-unknown { + background-color: #f5f5f5 !important; + border-left: 5px solid #aaaaaa !important; } diff --git a/ci.yml b/ci.yml index 0744170..eb6e167 100644 --- a/ci.yml +++ b/ci.yml @@ -10,6 +10,9 @@ on: - main jobs: + # ========================== + # 1) Build, unit/BDD tests, SonarCloud + # ========================== build-test-sonar: runs-on: ubuntu-latest @@ -56,8 +59,7 @@ jobs: - name: Build run: dotnet build --no-restore --configuration Release - # ---- Tests ---- - # Unit tests with coverage (for SonarCloud) + # ---- Tests (unit + BDD only here) ---- - name: Unit tests with coverage run: > dotnet test BPCalculator.UnitTests/BPCalculator.UnitTests.csproj @@ -66,11 +68,45 @@ jobs: /p:CoverletOutput=./TestResults/ /p:CoverletOutputFormat=opencover - # BDD tests - name: BDD tests run: > dotnet test BPCalculator.BddTests/BPCalculator.BddTests.csproj --no-build --configuration Release + # ---- End tests ---- + + # ---- SonarCloud end ---- + - name: SonarCloud - End analysis + run: dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN" + # ------------------------ + + + # ========================== + # 2) UI tests (Playwright + Selenium) + # ========================== + ui-tests: + name: Playwright & Selenium UI Tests + runs-on: ubuntu-latest + needs: build-test-sonar # run only if build-test-sonar succeeds + + # For Playwright/Selenium to hit the deployed app, set this + # to your real app URL (or keep localhost if you later start the app in the job) + env: + APP_URL: ${{ secrets.APP_URL }} # or "https://yourapp.azurewebsites.net" + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release # Playwright UI tests - name: Playwright tests @@ -83,9 +119,3 @@ jobs: run: > dotnet test BPCalculator.SeleniumTests/BPCalculator.SeleniumTests.csproj --no-build --configuration Release - # ---- End tests ---- - - # ---- SonarCloud end ---- - - name: SonarCloud - End analysis - run: dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN" - # ------------------------ From f18642c168f78384be1bdf277c807d5ce993c9a1 Mon Sep 17 00:00:00 2001 From: kaycan26 Date: Sat, 22 Nov 2025 22:13:19 +0000 Subject: [PATCH 6/6] Update Index.cshtml.cs --- BPCalculator/Pages/Index.cshtml.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BPCalculator/Pages/Index.cshtml.cs b/BPCalculator/Pages/Index.cshtml.cs index ad69809..bf5ffba 100644 --- a/BPCalculator/Pages/Index.cshtml.cs +++ b/BPCalculator/Pages/Index.cshtml.cs @@ -23,6 +23,8 @@ public class BloodPressureModel : PageModel public string BMICssClass { get; private set; } public string BMIAdvice { get; private set; } + // trigger workflow run + // setup initial data public void OnGet() {