Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SpecFlow structure added and initial accessibility test created #17

Merged
merged 41 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
15f3283
structure and tests for specflow accessibility checks
antk43 Jul 9, 2024
59f5f4e
running exe to launch web app
antk43 Jul 9, 2024
98198ea
switched to run github workflow on windows-latest
antk43 Jul 10, 2024
93764ea
initial basic readme version added to acceptance tests
antk43 Jul 10, 2024
f50281d
Merge branch 'main' into AO/specflow-accessibility-tests
antk43 Jul 15, 2024
e6d8364
readme updated and privacy page accessibility test added
antk43 Jul 15, 2024
557a55d
integrated selenium context and factory code from sd suite
antk43 Jul 17, 2024
06695ff
Merge branch 'main' into AO/specflow-accessibility-tests
antk43 Jul 17, 2024
2f9a509
merged main
antk43 Jul 17, 2024
88e47c6
moved search compoenent into pageobject model dir
antk43 Jul 17, 2024
a8d0196
acessibility exclude for hidden dev on search page
antk43 Jul 17, 2024
7fa8ab8
Merge branch 'main' into AO/specflow-accessibility-tests
antk43 Jul 23, 2024
5927de5
added base page along with selenium helpers
antk43 Jul 23, 2024
20dd1dc
added page integration test structure
antk43 Jul 23, 2024
a6b4420
merged main
antk43 Jul 23, 2024
3aebe73
structure changes
antk43 Jul 23, 2024
34acc48
added search form page integration test
antk43 Jul 25, 2024
0e4cf54
no results returned test added
antk43 Jul 25, 2024
0f1ade7
search establishment elements test added
antk43 Jul 29, 2024
3e1679c
dummy search service adator created and tests modified
antk43 Jul 29, 2024
0b670de
dotnet run added back into specflow hooks
antk43 Jul 29, 2024
f85d828
temporary removal of accessibility test and app start through hooks
antk43 Jul 29, 2024
210ba38
commented out after hook
antk43 Jul 29, 2024
b7735bb
reset
antk43 Jul 29, 2024
1063e5e
Merge branch 'main' into AO/specflow-accessibility-tests
antk43 Jul 30, 2024
8e3a1db
Merge branch 'main' into AO/specflow-accessibility-tests
antk43 Jul 30, 2024
b392aed
experimentation of dotnet run for accessibility test in pipeline
antk43 Jul 30, 2024
9a2892e
header link test added
antk43 Jul 30, 2024
e997754
ignored accessibility test and hook to start web
antk43 Jul 30, 2024
7dad5fc
hooks and accessibility test back in
antk43 Jul 30, 2024
e4b1c9c
Merge branch 'main' into AO/specflow-accessibility-tests
antk43 Jul 30, 2024
f53a070
added establishment address and type to dummy search after merging main
antk43 Jul 30, 2024
476777f
web app factory fixture implemented
antk43 Jul 31, 2024
b92d6b8
set default url and updated home page step
antk43 Jul 31, 2024
645e7ba
search results page accessibility test added
antk43 Jul 31, 2024
d9331e8
fixed accessibility failure - search results list
CathLass Jul 31, 2024
e05b4b1
ignore test until pr merged
antk43 Jul 31, 2024
e78c92c
merged bug fix to search results
antk43 Jul 31, 2024
0d552e4
final clean up
antk43 Jul 31, 2024
5fadba2
chrome browser by default
antk43 Jul 31, 2024
f3a54f2
add prerequisites to Readme for tests
CathLass Jul 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:
jobs:
build:

runs-on: ubuntu-latest
runs-on: windows-latest

steps:
- uses: actions/checkout@v4
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Xunit.Abstractions;

namespace Dfe.Data.SearchPrototype.Web.Tests.Acceptance.Drivers;

public interface IWebDriverContext : IDisposable
{
IWebDriver Driver { get; }
IWait<IWebDriver> Wait { get; }
void GoToUri(string path);
void GoToUri(string baseUri, string path);
void TakeScreenshot(ITestOutputHelper logger, string testName);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using OpenQA.Selenium;

namespace Dfe.Data.SearchPrototype.Web.Tests.Acceptance.Drivers;

public interface IWebDriverFactory
{
Lazy<IWebDriver> CreateDriver();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using OpenQA.Selenium.Support.UI;
using OpenQA.Selenium;
using System.Drawing;
using Dfe.Data.SearchPrototype.Web.Tests.Acceptance.Options;
using Microsoft.Extensions.Options;
using Xunit.Abstractions;

namespace Dfe.Data.SearchPrototype.Web.Tests.Acceptance.Drivers;

public class WebDriverContext : IWebDriverContext
{
private readonly WebDriverOptions _driverOptions;
private readonly Lazy<IWebDriver> _driver;
private readonly Lazy<IWait<IWebDriver>> _wait;
private readonly string _baseUri;

public IWebDriver Driver => _driver.Value;
public IWait<IWebDriver> Wait => _wait.Value;
private Type[] IgnoredExceptions { get; } = new[] { typeof(StaleElementReferenceException) };

public WebDriverContext(
IWebDriverFactory factory,
IOptions<WebOptions> options,
IOptions<WebDriverOptions> driverOptions
)
{
_driver = factory?.CreateDriver() ?? throw new ArgumentNullException(nameof(factory));
_baseUri = options.Value.GetWebUri() ?? throw new ArgumentNullException(nameof(options));
_wait = new(() => InitializeWait(Driver, IgnoredExceptions));
_driverOptions = driverOptions.Value;
}

private IJavaScriptExecutor JsExecutor => Driver as IJavaScriptExecutor ?? throw new ArgumentNullException(nameof(IJavaScriptExecutor));

/// <summary>
/// Navigate to relative path
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public void GoToUri(string path) => GoToUri(_baseUri, path);

/// <summary>
/// Navigate to a uri
/// </summary>
/// <param name="baseUri">baseUri for site e.g https://google.co.uk</param>
/// <param name="path">path from baseUri defaults to '/' e.g '/login'</param>
/// <exception cref="ArgumentNullException"></exception>
public void GoToUri(string baseUri, string path = "/")
{
_ = baseUri ?? throw new ArgumentNullException(nameof(baseUri));
var absoluteUri = $"{baseUri.TrimEnd('/')}{path}";
if (Uri.TryCreate(absoluteUri, UriKind.Absolute, out var uri))
{
Driver.Navigate().GoToUrl(uri);
}
else
{
throw new ArgumentException(nameof(absoluteUri));
}
}

/// <summary>
/// Dispose of <see cref="IWebDriver"/>
/// </summary>
public void Dispose()
{
using (Driver)
{
Driver.Quit();
}
}

public void TakeScreenshot(
ITestOutputHelper logger,
string testName
)
{

// Allows alternative path
var baseScreenshotDirectory =
Path.IsPathFullyQualified(_driverOptions.ScreenshotsDirectory) ?
_driverOptions.ScreenshotsDirectory :
Path.Combine(Directory.GetCurrentDirectory(), _driverOptions.ScreenshotsDirectory);

Directory.CreateDirectory(baseScreenshotDirectory);

var outputPath = Path.Combine(
baseScreenshotDirectory,
testName + ".png"
);

// Maximise viewport
Driver.Manage().Window.Size = new Size(
width: GetBrowserWidth(),
height: GetBrowserHeight()
);

// Screenshot
(Driver as ITakesScreenshot)?
.GetScreenshot()
.SaveAsFile(outputPath);

logger.WriteLine($"SCREENSHOT SAVED IN LOCATION: {outputPath}");
}

private static IWait<IWebDriver> InitializeWait(IWebDriver driver, Type[] ignoredExceptions)
{
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(20));
wait.IgnoreExceptionTypes(ignoredExceptions);
return wait;
}

private int GetBrowserWidth() =>
// Math.max returns a 64 bit number requiring casting
(int)(long)JsExecutor.ExecuteScript(
@"return Math.max(
window.innerWidth,
document.body.scrollWidth,
document.documentElement.scrollWidth)"
);

private int GetBrowserHeight() =>
// Math.max returns a 64 bit number requiring casting
(int)(long)JsExecutor.ExecuteScript(
@"return Math.max(
window.innerHeight,
document.body.scrollHeight,
document.documentElement.scrollHeight)"
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using SeleniumExtras.WaitHelpers;
using Xunit;

namespace Dfe.Data.SearchPrototype.Web.Tests.Acceptance.Drivers;

public static class WebDriverExtensions
{
public static void ElementDoesNotExist(this IWebDriverContext context, Func<IWebElement> locate)
{
context.Wait.Timeout = TimeSpan.FromSeconds(4);
Assert.ThrowsAny<WebDriverTimeoutException>(locate);
}

public static IWebElement UntilElementContainsText(this IWebElement element, IWebDriverContext context, string text)
{
_ = text ?? throw new ArgumentNullException(nameof(text));
context.Wait.Message = $"Element did not contain text {text}";
context.Wait.Until(t => element.Text.Contains(text));
context.Wait.Message = string.Empty;
return element;
}

public static IWebElement UntilElementTextIs(IWebElement element, IWait<IWebDriver> wait, string text)
{
_ = text ?? throw new ArgumentNullException(nameof(text));
wait.Message = $"Element did not equal text {text}";
wait.Until(t => element.Text.Contains(text));
wait.Message = string.Empty;
return element;
}

public static IWebElement UntilAriaExpandedIs(this IWait<IWebDriver> wait, bool isExpanded, By locator)
{
var element = wait.UntilElementExists(locator);
wait.Until(_ => element.GetAttribute("aria-expanded") == (isExpanded ? "true" : "false"));
return element;
}
public static IWebElement UntilElementExists(this IWait<IWebDriver> wait, By by) => wait.Until(ExpectedConditions.ElementExists(by));

public static IWebElement UntilElementIsVisible(this IWait<IWebDriver> wait, By by) => wait.Until(ExpectedConditions.ElementIsVisible(by));

public static IWebElement UntilElementIsClickable(this IWait<IWebDriver> wait, By by) => wait.Until(ExpectedConditions.ElementToBeClickable(by));
public static IReadOnlyList<IWebElement> UntilMultipleElementsExist(this IWait<IWebDriver> wait, By by) => wait.Until(ExpectedConditions.PresenceOfAllElementsLocatedBy(by));
public static IReadOnlyList<IWebElement> UntilMultipleElementsVisible(this IWait<IWebDriver> wait, By by) => wait.Until(ExpectedConditions.VisibilityOfAllElementsLocatedBy(by));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium;
using Microsoft.Extensions.Options;
using System.Drawing;
using Dfe.Data.SearchPrototype.Web.Tests.Acceptance.Options;

namespace Dfe.Data.SearchPrototype.Web.Tests.Acceptance.Drivers;


public sealed class WebDriverFactory : IWebDriverFactory
{
private static readonly IEnumerable<string> DEFAULT_OPTIONS = new[]
{
"--incognito",
"--safebrowsing-disable-download-protection",
"--no-sandbox",
"--start-maximized",
"--start-fullscreen"
};

private static readonly Dictionary<string, (int x, int y)> MOBILE_VIEWPORTS = new()
{
{ "desktop", (1920, 1080) },
{ "iphone14", (390, 844) },
{ "iphone11", (414, 896) }
};

private static TimeSpan DEFAULT_PAGE_LOAD_TIMEOUT = TimeSpan.FromSeconds(30);
private readonly WebDriverOptions _webDriverOptions;
private readonly WebDriverSessionOptions _sessionOptions;

public WebDriverFactory(
IOptions<WebDriverOptions> webDriverOptions,
WebDriverSessionOptions sessionOptions
)
{
_webDriverOptions = webDriverOptions?.Value ?? throw new ArgumentNullException(nameof(webDriverOptions));
_sessionOptions = sessionOptions ?? throw new ArgumentNullException(nameof(_sessionOptions));
}

public Lazy<IWebDriver> CreateDriver()
{
// viewports are expressed as cartesian coordinates (x,y)
var viewportDoesNotExist = !MOBILE_VIEWPORTS.TryGetValue(_sessionOptions.Device, out var viewport);
if (viewportDoesNotExist)
{
throw new ArgumentException($"device value {_sessionOptions.Device} has no mapped viewport");
}
var (width, height) = viewport;

return new Lazy<IWebDriver>(() =>
{
_webDriverOptions.DriverBinaryDirectory ??= Directory.GetCurrentDirectory();
IWebDriver driver = _sessionOptions switch
{
{ DisableJs: true } or { Browser: "firefox" } => CreateFirefoxDriver(_webDriverOptions, _sessionOptions),
_ => CreateChromeDriver(_webDriverOptions)
};
driver.Manage().Window.Size = new Size(width, height);
driver.Manage().Cookies.DeleteAllCookies();
driver.Manage().Timeouts().PageLoad = DEFAULT_PAGE_LOAD_TIMEOUT;
return driver;
});
}

private static ChromeDriver CreateChromeDriver(
WebDriverOptions driverOptions
)
{
ChromeOptions option = new();
option.AddArguments(DEFAULT_OPTIONS);

// chromium based browsers using new headless switch https://www.selenium.dev/blog/2023/headless-is-going-away/
if (driverOptions.Headless)
{
option.AddArgument("--headless=new");
}
option.AddUserProfilePreference("safebrowsing.enabled", true);
option.AddUserProfilePreference("download.prompt_for_download", false);
option.AddUserProfilePreference("disable-popup-blocking", "true");
option.AddArgument("--window-size=1920,1080");
return new ChromeDriver(driverOptions.DriverBinaryDirectory, option);
}

private static FirefoxDriver CreateFirefoxDriver(
WebDriverOptions driverOptions,
WebDriverSessionOptions sessionOptions
)
{
var options = new FirefoxOptions
{
// TODO load TLS cert into firefox options
AcceptInsecureCertificates = true,
EnableDevToolsProtocol = true,
};
options.AddArguments(DEFAULT_OPTIONS);
if (driverOptions.Headless)
{
options.AddArgument("--headless");
}
if (sessionOptions.DisableJs)
{
options.SetPreference("javascript.enabled", false);
}
return new(driverOptions.DriverBinaryDirectory, options);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Text.RegularExpressions;

namespace Dfe.Data.SearchPrototype.Web.Tests.Acceptance.Extensions;

public static class StringExtensions
{
public static readonly Regex HtmlReplacer = new("<[^>]*>");
public static string? ToLowerRemoveHyphens(this string? str)
=> string.IsNullOrEmpty(str) ? str : str.Replace(' ', '-').ToLower();

public static string? ReplaceHTML(this string? str)
=> string.IsNullOrEmpty(str) ? str : HtmlReplacer.Replace(str, string.Empty);

public static string SanitiseToHTML(string input)
{
var ouput = input.Replace("\"", "\'");
return ouput;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Feature: AccessibilityTests

@ignore
Scenario: Homepage accessibility
When the user views the home page
Then the home page is accessible
Loading