diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml
index 1a34972f..b0bd8295 100644
--- a/.github/workflows/build-and-deploy.yml
+++ b/.github/workflows/build-and-deploy.yml
@@ -514,6 +514,73 @@ jobs:
smoke-test: false
service: ${{ vars.TEAMS_MSG_SERVICE_NAME }}
+ post-deploy-tests:
+ name: Run Integration Tests on Deployed Test Environment
+ needs: deploy
+ if: ${{ matrix.environment == 'test' }}
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ # Set up .NET
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Install Playwright browsers (.NET)
+ run: pwsh ./Tests/SAPPub.Integration.Tests/bin/Release/net8.0/playwright.ps1 install
+
+ - name: Run Integration Tests with Coverage
+ env:
+ HEADED: 0
+ PLAYWRIGHT_IGNORE_HTTPS_ERRORS: true
+ ASPNETCORE_ENVIRONMENT: CI
+ run:
+ dotnet test Tests/SAPPub.Integration.Tests/SAPPub.IntegrationTests.csproj \
+ --no-build \
+ --configuration Release \
+ --results-directory "TestResults" \
+ --logger "trx;LogFileName=$(basename "$proj" .csproj).trx" \
+ --collect "XPlat Code Coverage"\
+ /p:CollectCoverage=true \
+ /p:CoverletOutputFormat=cobertura \
+ /p:DeterministicReport=true \
+ /p:UseSourceLink=true \
+ /p:Exclude='[*]AspNetCoreGeneratedDocument.*%2c[*]AspNetCore.Views.*' \
+ /p:ExcludeByAttribute='GeneratedCodeAttribute*%2cObsoleteAttribute*%2cExcludeFromCodeCoverage' \
+ --verbosity normal
+ done
+
+ - name: Upload Test Result Files
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: test-results
+ path: "**/TestResults/**/*"
+ retention-days: 5
+
+ - name: Upload UI artifacts on failure
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: ui-test-artifacts
+ path: |
+ **/test-artifacts/screenshots/**
+ **/test-artifacts/videos/**
+ **/test-artifacts/traces/**
+
+ - name: Publish Test Results
+ uses: dorny/test-reporter@v2
+ if: always()
+ with:
+ reporter: 'dotnet-trx'
+ path: "**/TestResults/**/*.trx"
+ name: 'Test Results'
+
# ---------------------------
# MANUAL DEPLOY
# ---------------------------
diff --git a/SAPPub.Web/Program.cs b/SAPPub.Web/Program.cs
index b09328f2..2e8decca 100644
--- a/SAPPub.Web/Program.cs
+++ b/SAPPub.Web/Program.cs
@@ -85,12 +85,14 @@ public static void Main(string[] args)
// Only required for real runtime environments
if (builder.Environment.IsDevelopment() || builder.Environment.IsProduction() || builder.Environment.IsStaging())
throw new InvalidOperationException("Connection string 'PostgresConnectionString' is not configured.");
-
- // For Testing/UITests: use a harmless dummy so nothing accidentally connects
- connectionString = "Host=127.0.0.1;Port=1;Database=x;Username=x;Password=x;Timeout=1;Command Timeout=1";
}
- builder.Services.AddSingleton(_ => NpgsqlDataSource.Create(connectionString));
+ builder.Services.AddSingleton(_ =>
+ {
+ var builder = new NpgsqlDataSourceBuilder(connectionString);
+ builder.EnableParameterLogging();
+ return builder.Build();
+ });
builder.Services.AddDependencies(builder.Environment, builder.Configuration);
builder.Services.AddLuceneDependencies();
diff --git a/SAPPub.Web/Views/SecondarySchool/AcademicPerformanceEnglishAndMathsResults.cshtml b/SAPPub.Web/Views/SecondarySchool/AcademicPerformanceEnglishAndMathsResults.cshtml
index afd65595..d2109f52 100644
--- a/SAPPub.Web/Views/SecondarySchool/AcademicPerformanceEnglishAndMathsResults.cshtml
+++ b/SAPPub.Web/Views/SecondarySchool/AcademicPerformanceEnglishAndMathsResults.cshtml
@@ -91,6 +91,8 @@
@(Model.SelectedGrade == GcseGradeDataSelection.Grade5AndAbove ? "Grade 5 is comparable to the top of the old grade C." : "The bottom of grade 4 is comparable to the bottom of the old grade C")
+ @if(false)
+ {
diff --git a/SAPPub.Web/appsettings.Development.json b/SAPPub.Web/appsettings.Development.json
index 027d3798..37653a82 100644
--- a/SAPPub.Web/appsettings.Development.json
+++ b/SAPPub.Web/appsettings.Development.json
@@ -6,6 +6,9 @@
}
},
"ConnectionStrings": {
- "PostgresConnectionString": "Host=localhost;Port=5432;Database=SAPData;Username=postgres;Password=postgres"
+ "PostgresConnectionString": ""
+ },
+ "Playwright": {
+ "Headed": true
}
}
diff --git a/SAPPub.sln b/SAPPub.sln
index 44933707..63df417f 100644
--- a/SAPPub.sln
+++ b/SAPPub.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 18
-VisualStudioVersion = 18.1.11312.151
+# Visual Studio Version 17
+VisualStudioVersion = 17.14.37027.9
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SAPPub.Web", "SAPPub.Web\SAPPub.Web.csproj", "{24624574-4DE4-4A91-8273-7AE3EEE13537}"
EndProject
@@ -28,6 +28,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SAPData", "SAPData\SAPData.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SAPData.Unit.Tests", "SAPData.Tests.Unit\SAPData.Unit.Tests.csproj", "{2FD6F8C0-9D97-7714-4445-AD6E47BEB671}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SAPPub.IntegrationTests", "Tests\SAPPub.Integration.Tests\SAPPub.IntegrationTests.csproj", "{D90BBB38-FA34-5ABD-F17F-F30AF1D84C6A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -66,6 +68,10 @@ Global
{2FD6F8C0-9D97-7714-4445-AD6E47BEB671}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2FD6F8C0-9D97-7714-4445-AD6E47BEB671}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2FD6F8C0-9D97-7714-4445-AD6E47BEB671}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D90BBB38-FA34-5ABD-F17F-F30AF1D84C6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D90BBB38-FA34-5ABD-F17F-F30AF1D84C6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D90BBB38-FA34-5ABD-F17F-F30AF1D84C6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D90BBB38-FA34-5ABD-F17F-F30AF1D84C6A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -75,6 +81,7 @@ Global
{EB1D1EAE-E264-4FF5-ACB9-8614FD694C69} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{5F164DC3-0185-4D92-B689-7A004F81C232} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{2FD6F8C0-9D97-7714-4445-AD6E47BEB671} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ {D90BBB38-FA34-5ABD-F17F-F30AF1D84C6A} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0D7E6B09-2E5D-4BA4-899B-FBCC5A7F92CD}
diff --git a/Tests/SAPPub.Integration.Tests/Infrastructure/BasePageTest.cs b/Tests/SAPPub.Integration.Tests/Infrastructure/BasePageTest.cs
new file mode 100644
index 00000000..d3faba69
--- /dev/null
+++ b/Tests/SAPPub.Integration.Tests/Infrastructure/BasePageTest.cs
@@ -0,0 +1,77 @@
+using Microsoft.Playwright;
+using Microsoft.Playwright.Xunit;
+
+namespace SAPPub.Integration.Tests;
+
+public abstract class BasePageTest : PageTest
+{
+ private readonly WebApplicationSetupFixture _fixture;
+
+ // ReSharper disable once ConvertToPrimaryConstructor
+ protected BasePageTest(WebApplicationSetupFixture fixture)
+ {
+ _fixture = fixture;
+
+ if (_fixture.IsHeaded())
+ {
+ Environment.SetEnvironmentVariable("HEADED", "1");
+ }
+ }
+
+ public override BrowserNewContextOptions ContextOptions()
+ {
+ return new BrowserNewContextOptions
+ {
+ BaseURL = _fixture.BaseUrl.TrimEnd('/'),
+ IgnoreHTTPSErrors = true,
+ ViewportSize = new() { Width = 1280, Height = 720 },
+ Locale = "en-GB",
+ TimezoneId = "Europe/London",
+ JavaScriptEnabled = true,
+ };
+ }
+
+ public override async Task InitializeAsync()
+ {
+ await base.InitializeAsync();
+
+ Page.SetDefaultTimeout((float)TimeSpan.FromSeconds(60).TotalMilliseconds);
+ Page.SetDefaultNavigationTimeout((float)TimeSpan.FromSeconds(100).TotalMilliseconds);
+ }
+
+ public async Task WaitForSearchInputsAsync(int timeoutMs = 5000)
+ {
+ var selector = "input[name='__Query'], input[name='Query'][type='hidden'], input[name='Query']";
+ await Page.WaitForSelectorAsync(selector, new() { Timeout = timeoutMs });
+ await Page.WaitForTimeoutAsync(100);
+ }
+ public async Task
GetQueryInputLocatorAsync(int checkTimeoutMs = 1000)
+ {
+ var jsLocator = Page.Locator("input[name='__Query']");
+ try
+ {
+ if (await jsLocator.CountAsync() > 0)
+ {
+ var isVisible = await jsLocator.IsVisibleAsync();
+ if (isVisible) return jsLocator;
+ }
+
+ var serverLocator = Page.Locator("input[name='Query']");
+ if (await serverLocator.CountAsync() > 0) return serverLocator;
+
+ var found = await Page.WaitForSelectorAsync("input[name='__Query'], input[name='Query']", new() { Timeout = checkTimeoutMs });
+ if (found != null)
+ {
+ var nameAttr = await found.GetAttributeAsync("name");
+ if (nameAttr == "__Query")
+ return Page.Locator("input[name='__Query']");
+ return Page.Locator("input[name='Query']");
+ }
+ return Page.Locator("input[name='Query']");
+ }
+ catch
+ {
+ return Page.Locator("input[name='Query']");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tests/SAPPub.Integration.Tests/Infrastructure/IntegrationTestCollection.cs b/Tests/SAPPub.Integration.Tests/Infrastructure/IntegrationTestCollection.cs
new file mode 100644
index 00000000..77a9c08a
--- /dev/null
+++ b/Tests/SAPPub.Integration.Tests/Infrastructure/IntegrationTestCollection.cs
@@ -0,0 +1,7 @@
+namespace SAPPub.Integration.Tests;
+
+[CollectionDefinition("Integration Tests", DisableParallelization = true)]
+public class IntegrationTestCollection : ICollectionFixture
+{
+}
+
diff --git a/Tests/SAPPub.Integration.Tests/Infrastructure/TestWebApplicationFactory.cs b/Tests/SAPPub.Integration.Tests/Infrastructure/TestWebApplicationFactory.cs
new file mode 100644
index 00000000..987864be
--- /dev/null
+++ b/Tests/SAPPub.Integration.Tests/Infrastructure/TestWebApplicationFactory.cs
@@ -0,0 +1,211 @@
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using SAPPub.Web;
+
+namespace SAPPub.Integration.Tests;
+
+public class TestWebApplicationFactory : WebApplicationFactory
+{
+ private IHost? _host;
+ private static string? _cachedWebProjectPath;
+
+ public IConfiguration GetAppConfiguration()
+ {
+ using var scope = Services.CreateScope();
+ return scope.ServiceProvider.GetRequiredService();
+ }
+
+ protected override void ConfigureWebHost(IWebHostBuilder builder)
+ {
+ builder.UseUrls("http://127.0.0.1:0", "https://127.0.0.1:0");
+
+ // Set content root to web project so static files (wwwroot) are found
+ //var webProjectPath = GetWebProjectPath();
+ //builder.UseContentRoot(webProjectPath);
+ //builder.UseWebRoot(Path.Combine(webProjectPath, "wwwroot"));
+
+
+ //---------------------------------------------------------------------------------------
+ // shouldn't need any of this - pipeline should use the web app's standard config
+ // but leaving here for now in case we need to override config or services for testing purposes
+ // ------------------------------------------------------------------------------------------------
+ //var testDataFilePath = GetTestDataFilePath();
+ //var configurationValues = CreateConfigurationValues(testDataFilePath);
+ //var configuration = new ConfigurationBuilder()
+ // .AddInMemoryCollection(configurationValues)
+ // .Build();
+
+ //builder
+ // .UseConfiguration(configuration)
+ // .ConfigureAppConfiguration(configurationBuilder =>
+ // {
+ // configurationBuilder.AddInMemoryCollection(configurationValues);
+ // })
+ // .ConfigureServices(services =>
+ // {
+ // services.RemoveAll(typeof(IGenericRepository<>));
+ // services.AddSingleton(typeof(IGenericRepository<>), typeof(FakeGenericRepository<>));
+ // });
+
+ //builder.ConfigureAppConfiguration((context, config) =>
+ //{
+ // config.AddJsonFile("appsettings.json")
+ // .AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true);
+ //});
+ }
+
+ protected override IHost CreateHost(IHostBuilder builder)
+ {
+ // Use web project path for content root (for static files)
+ var webProjectPath = GetWebProjectPath();
+ builder.UseContentRoot(webProjectPath);
+
+ // Create the host for TestServer
+ var testHost = builder.Build();
+
+ // Modify the host builder to use Kestrel with correct content root
+ builder.ConfigureWebHost(webHostBuilder =>
+ {
+ webHostBuilder.UseKestrel();
+ webHostBuilder.UseContentRoot(webProjectPath);
+ webHostBuilder.UseWebRoot(Path.Combine(webProjectPath, "wwwroot"));
+ });
+
+ // Create and start the Kestrel server
+ _host = builder.Build();
+ _host.Start();
+
+ // Extract the selected dynamic port
+ var server = _host.Services.GetRequiredService();
+ var addresses = server.Features.Get();
+
+ ClientOptions.BaseAddress = addresses!.Addresses
+ .Select(x => new Uri(x))
+ .Last();
+
+ LogStartupInfo(webProjectPath);
+
+ testHost.Start();
+ return testHost;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _host?.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Path Resolution
+
+ private static string GetWebProjectPath()
+ {
+ // Cache the path to avoid repeated file system walks
+ if (_cachedWebProjectPath != null)
+ {
+ return _cachedWebProjectPath;
+ }
+
+ var currentDir = AppContext.BaseDirectory;
+ var directory = new DirectoryInfo(currentDir);
+
+ // Walk up to find solution root
+ while (directory != null && !directory.GetFiles("*.sln").Any())
+ {
+ directory = directory.Parent;
+ }
+
+ if (directory == null)
+ {
+ throw new InvalidOperationException(
+ $"Could not find solution root from {currentDir}");
+ }
+
+ // Find web project
+ var possiblePaths = new[]
+ {
+ Path.Combine(directory.FullName, "SAPPub.Web"),
+ Path.Combine(directory.FullName, "src", "SAPPub.Web"),
+ };
+
+ _cachedWebProjectPath = possiblePaths.FirstOrDefault(Directory.Exists)
+ ?? throw new InvalidOperationException(
+ $"Could not find SAPPub.Web. Searched: {string.Join(", ", possiblePaths)}");
+
+ return _cachedWebProjectPath;
+ }
+
+ private static string GetTestDataFilePath()
+ {
+ // First try test project's TestData folder
+ var testProjectPath = Path.Combine(
+ AppContext.BaseDirectory,
+ "UI",
+ "TestData",
+ "Establishments-UI-Test-Data.csv");
+
+ if (File.Exists(testProjectPath))
+ {
+ return testProjectPath;
+ }
+
+ // Try web project's TestData folder
+ var webProjectPath = Path.Combine(
+ GetWebProjectPath(),
+ "UI",
+ "TestData",
+ "Establishments-UI-Test-Data.csv");
+
+ if (File.Exists(webProjectPath))
+ {
+ return webProjectPath;
+ }
+
+ throw new FileNotFoundException(
+ $"Test data file not found. Searched:\n- {testProjectPath}\n- {webProjectPath}");
+ }
+
+ #endregion
+
+ #region Configuration
+
+ private static Dictionary CreateConfigurationValues(string testDataFilePath)
+ {
+ return new Dictionary
+ {
+ { "Establishments:CsvPath", testDataFilePath },
+ };
+ }
+
+ #endregion
+
+ #region Logging
+
+ private void LogStartupInfo(string webProjectPath)
+ {
+ var wwwrootPath = Path.Combine(webProjectPath, "wwwroot");
+ var jsPath = Path.Combine(wwwrootPath, "js");
+
+ if (Directory.Exists(jsPath))
+ {
+ var jsFiles = Directory.GetFiles(jsPath, "*.js", SearchOption.AllDirectories);
+ // Check for key files
+ var autocomplete = Path.Combine(jsPath, "accessible-autocomplete.min.js");
+ var debounce = Path.Combine(jsPath, "lodash.debounce", "index.js");
+
+ }
+ else
+ {
+ Console.WriteLine($"❌ JS folder not found: {jsPath}");
+ }
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/Tests/SAPPub.Integration.Tests/Infrastructure/WebApplicationSetupFixture.cs b/Tests/SAPPub.Integration.Tests/Infrastructure/WebApplicationSetupFixture.cs
new file mode 100644
index 00000000..f46f6663
--- /dev/null
+++ b/Tests/SAPPub.Integration.Tests/Infrastructure/WebApplicationSetupFixture.cs
@@ -0,0 +1,34 @@
+namespace SAPPub.Integration.Tests;
+
+public class WebApplicationSetupFixture : IAsyncLifetime
+{
+ private TestWebApplicationFactory? _factory;
+
+ public string BaseUrl { get; private set; } = null!;
+
+ public Task InitializeAsync()
+ {
+ _factory = new TestWebApplicationFactory();
+
+ if (_factory.Server == null) throw new InvalidOperationException("Test Server not started");
+
+ BaseUrl = _factory.ClientOptions.BaseAddress.ToString();
+
+ return Task.CompletedTask;
+ }
+
+ public bool IsHeaded()
+ {
+ return _factory?
+ .GetAppConfiguration()
+ .GetSection("Playwright")["Headed"] is string headed && bool.TryParse(headed, out var isHeaded) && isHeaded;
+ }
+
+ public async Task DisposeAsync()
+ {
+ if (_factory != null)
+ {
+ await _factory.DisposeAsync();
+ }
+ }
+}
diff --git a/Tests/SAPPub.Integration.Tests/SAPPub.IntegrationTests.csproj b/Tests/SAPPub.Integration.Tests/SAPPub.IntegrationTests.csproj
new file mode 100644
index 00000000..32c57a03
--- /dev/null
+++ b/Tests/SAPPub.Integration.Tests/SAPPub.IntegrationTests.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/SAPPub.Integration.Tests/SecondarySchoolTests/AboutSchoolPageTests.cs b/Tests/SAPPub.Integration.Tests/SecondarySchoolTests/AboutSchoolPageTests.cs
new file mode 100644
index 00000000..adf075d6
--- /dev/null
+++ b/Tests/SAPPub.Integration.Tests/SecondarySchoolTests/AboutSchoolPageTests.cs
@@ -0,0 +1,154 @@
+using SAPPub.Integration.Tests;
+
+namespace SAPPub.IntegrationTests.SecondarySchoolTests;
+
+[Collection("Integration Tests")]
+public class AboutSchoolPageTests(WebApplicationSetupFixture fixture) : BasePageTest(fixture)
+{
+ private string PageUrl(string urn) => $"/school/{urn}";
+
+ [Fact]
+ public async Task AboutSchoolPage_LoadsSuccessfully()
+ {
+ // Arrange && Act
+ var response = await Page.GotoAsync(PageUrl("105574"));
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Equal(200, response.Status);
+ }
+
+ [Fact]
+ public async Task AboutSchoolPage_HasCorrectTitle()
+ {
+ // PageUrl
+ await Page.GotoAsync(PageUrl("105574"));
+
+ // Act
+ var title = await Page.TitleAsync();
+
+ // Assert
+ Assert.Contains("About the school", title);
+ }
+
+ [Fact]
+ public async Task AboutSchoolPage_DisplaysMainHeading()
+ {
+ // Arrange
+ await Page.GotoAsync(PageUrl("105574"));
+
+ // Act
+ var heading = await Page.Locator("h1").TextContentAsync();
+
+ // Assert
+ Assert.NotNull(heading);
+ Assert.NotEmpty(heading!.Trim());
+ }
+
+ [Fact]
+ public async Task AboutSchoolPage_Displays_SchoolName_Caption()
+ {
+ // Arrange
+ await Page.GotoAsync(PageUrl("105574"));
+
+ // Act
+ var schoolNameCaptionLocator = Page.Locator("#school-name-caption");
+ var isVisible = await schoolNameCaptionLocator.IsVisibleAsync();
+ var schoolNameCaption = await schoolNameCaptionLocator.TextContentAsync();
+
+ // Assert
+ Assert.True(isVisible);
+ Assert.NotNull(schoolNameCaption);
+ Assert.Equal("Loreto High School Chorlton", schoolNameCaption);
+ }
+
+ [Theory]
+ [InlineData("114311", true, "31 December 2022")]
+ public async Task AboutSchoolPage_Displays_School_Closed_Info(string urn, bool isSchoolClosed, string? date)
+ {
+ // Act
+ await Page.GotoAsync(PageUrl(urn));
+
+ // Assert
+ var schoolClosedCard = Page.GetByTestId("school-closed-custom-card");
+
+ Assert.Equal(isSchoolClosed, await schoolClosedCard.IsVisibleAsync());
+
+ if (isSchoolClosed)
+ {
+ var value = await schoolClosedCard.Locator("p").TextContentAsync();
+
+ var expectedText = date != null ? $"This school closed on {date}" : "Closed";
+
+ Assert.NotNull(value);
+ Assert.Equal(expectedText, value.Trim());
+ }
+ }
+
+ [Theory]
+ [InlineData("105574", null)]
+ [InlineData("137552", "THE PASSMORES CO-OPERATIVE LEARNING COMMUNITY")]
+ public async Task AboutSchoolPage_DisplaysTrustNameRow_WhenTrustNameNotNull(string urn, string? trustName)
+ {
+ // Act
+ await Page.GotoAsync(PageUrl(urn));
+
+ // Assert
+ var detailsSummary = Page.Locator("#school-details-summary");
+
+ Assert.True(await detailsSummary.IsVisibleAsync());
+ var row = detailsSummary
+ .Locator(".govuk-summary-list__row")
+ .Filter(new() { Has = Page.Locator(".govuk-summary-list__key", new() { HasText = " Academy Trust " }) });
+
+ if (trustName != null)
+ {
+ var value = await row.Locator(".govuk-summary-list__value").TextContentAsync();
+ Assert.NotNull(value);
+ Assert.Equal(trustName, value.Trim());
+ }
+ else
+ {
+ Assert.False(await row.IsVisibleAsync());
+ }
+ }
+
+ [Fact]
+ public async Task AboutSchoolPage_DisplaysSchoolLocation()
+ {
+ // Arrange
+ await Page.GotoAsync(PageUrl("105574"));
+
+ // Act
+ var isVisible = await Page.Locator("#school-location-summary").IsVisibleAsync();
+
+ // Assert
+ Assert.True(isVisible);
+ }
+
+ [Fact]
+ public async Task AboutSchoolPage_DisplaysSpecialistUnit()
+ {
+ // Arrange
+ await Page.GotoAsync(PageUrl("105574"));
+
+ // Act
+ var isVisible = await Page.Locator("#details-sen").IsVisibleAsync();
+
+ // Assert
+ Assert.True(isVisible);
+ }
+
+ [Fact]
+ public async Task AboutSchoolPage_DisplaysSchoolFeatures()
+ {
+ // Arrange
+ await Page.GotoAsync(PageUrl("105574"));
+
+ // Act
+ var isVisible = await Page.Locator("#school-features-summary").IsVisibleAsync();
+
+ // Assert
+ Assert.True(isVisible);
+ }
+}
\ No newline at end of file
diff --git a/Tests/SAPPub.Integration.Tests/SecondarySchoolTests/AttainmentPageTests.cs b/Tests/SAPPub.Integration.Tests/SecondarySchoolTests/AttainmentPageTests.cs
new file mode 100644
index 00000000..6d946a5d
--- /dev/null
+++ b/Tests/SAPPub.Integration.Tests/SecondarySchoolTests/AttainmentPageTests.cs
@@ -0,0 +1,78 @@
+using Microsoft.Playwright;
+using SAPPub.Integration.Tests;
+using System.Text.RegularExpressions;
+
+namespace SAPPub.IntegrationTests.SecondarySchoolTests;
+
+[Collection("Integration Tests")]
+public class AttainmentPageTests(WebApplicationSetupFixture fixture) : BasePageTest(fixture)
+{
+ private string PageUrl(string urn) => $"/school/{urn}";
+
+ [Theory]
+ [InlineData("136745", 39.8, 44.1, 46)]
+ [InlineData("137638", 44.2, 44.1, 46)]
+ [InlineData("142894", 39.2, 44.1, 46)]
+ [InlineData("144496", 49.6, 44.1, 46)]
+ [InlineData("144991", 45.6, 44.1, 46)]
+ [InlineData("145564", 50.4, 44.1, 46)]
+ [InlineData("147531", 49.1, 44.1, 46)]
+ [InlineData("147894", 57.5, 44.1, 46)]
+ [InlineData("147711", 45.8, 44.1, 46)]
+ [InlineData("136971", 46.4, 44.1, 46)]
+ [InlineData("137903", 46.9, 44.1, 46)]
+ [InlineData("148109", 44.7, 44.1, 46)]
+ [InlineData("145253", 41.5, 44.1, 46)]
+ [InlineData("148706", 39.4, 44.1, 46)]
+ [InlineData("138717", 48.2, 44.1, 46)]
+ [InlineData("149962", 35.7, 44.1, 46)]
+ [InlineData("136770", 43.9, 44.1, 46)]
+ [InlineData("114308", 53.4, 44.1, 46)]
+ [InlineData("137696", 45.2, 44.1, 46)]
+ [InlineData("149251", 37.4, 44.1, 46)]
+ [InlineData("114312", 55.2, 44.1, 46)]
+ [InlineData("147122", 41.4, 44.1, 46)]
+ [InlineData("136451", 52.2, 44.1, 46)]
+ [InlineData("149739", 47.8, 44.1, 46)]
+ [InlineData("147670", 51.5, 44.1, 46)]
+ [InlineData("138075", 51.8, 44.1, 46)]
+ [InlineData("137702", 46.4, 44.1, 46)]
+ [InlineData("143583", 57.7, 44.1, 46)]
+ [InlineData("148304", 44.5, 44.1, 46)]
+ [InlineData("138172", 44.9, 44.1, 46)]
+ public async Task AboutSchoolPage_LoadsSuccessfully(string urn, double expectedAttainmentSchool, double expectedAttainmentLA, double expectedAttainmentEngland)
+ {
+ // Arrange && Act
+ var _ = await Page.GotoAsync(PageUrl(urn));
+ var response = await ClickAcademicPerformanceLinkAsync();
+
+ // Assert
+ var schoolAttainment8 = await GetScoreAsync("attainment8-establishment-card", "The attainment 8 score for this school is");
+ Assert.NotNull(schoolAttainment8);
+ Assert.Equal(expectedAttainmentSchool.ToString("F1"), schoolAttainment8);
+ }
+
+ private Task ClickAcademicPerformanceLinkAsync()
+ {
+ var response = Page.RunAndWaitForResponseAsync(
+ async () =>
+ {
+ await Page.GetByRole(AriaRole.Link, new() { Name = "Academic performance" }).ClickAsync();
+ },
+ response => response.Url.Contains("/academic-performance-attainment-and-progress") && response.Status == 200
+ );
+ return response;
+ }
+
+ public async Task GetScoreAsync(string dataTestid, string textString)
+ {
+ var card = Page.Locator($"[data-testid='{dataTestid}']");
+ var p = card.Locator("p.govuk-body", new() { HasTextString = textString });
+ var input = await p.InnerTextAsync();
+ var match = Regex.Matches(input, @"\d+(\.\d+)?")
+ .Cast()
+ .Last();
+
+ return match.Value;
+ }
+}
diff --git a/Tests/SAPPub.Web.Tests/UI/Infrastructure/BasePageTest.cs b/Tests/SAPPub.Web.Tests/UI/Infrastructure/BasePageTest.cs
index d571a7a7..ff1c83a9 100644
--- a/Tests/SAPPub.Web.Tests/UI/Infrastructure/BasePageTest.cs
+++ b/Tests/SAPPub.Web.Tests/UI/Infrastructure/BasePageTest.cs
@@ -13,7 +13,7 @@ protected BasePageTest(WebApplicationSetupFixture fixture)
_fixture = fixture;
//Uncomment to run tests in headed mode
- //Environment.SetEnvironmentVariable("HEADED", "1");
+ Environment.SetEnvironmentVariable("HEADED", "1");
}
public override BrowserNewContextOptions ContextOptions()