Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
66 changes: 64 additions & 2 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Builds the suite and runs it, so a change here is checked before a language
# repository picks it up.
#
# Two jobs, because they answer different questions. The first runs the tests
# Three jobs, because they answer different questions. The first runs the tests
# that need no browser, no cloud and no keys. The second starts a real browser
# on each runner the language repositories use, including the ARM64 Linux ones,
# and fails when one will not start, which is the fault the second job exists
# to catch.
# to catch. The third starts CloakBrowser in its vendor's container and runs
# the same check with a driver attached to it.
name: Build and test

on:
Expand Down Expand Up @@ -72,3 +73,64 @@ jobs:

- name: Start a browser
run: dotnet test -c Release --no-build --filter "TestCategory=Browser"

cloak:
name: CloakBrowser on ubuntu-latest
runs-on: ubuntu-latest
env:
# The vendor's image by digest, so the browser cannot change under the
# tag. The browser is closed source and only ever runs in this container.
CLOAK_IMAGE: cloakhq/cloakbrowser:0.5.12@sha256:2fdd1289154f30594c7bab74943ea7baa6b617a9ec026ab4bf339c2ebabcb712
CLOAK_DEBUGGER_ADDRESS: 127.0.0.1:9222
# Selenium Manager fetches the driver for the browser in the container
# and would otherwise download a Chrome of that version that nothing
# runs.
SE_AVOID_BROWSER_DOWNLOAD: 'true'
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

# Host networking lets the browser reach the page the tests serve on
# localhost. Update checks are off, so the browser that runs is the one
# in the image. Nothing from the runner is mounted and no secret is
# passed. The first request for the version starts the browser, and
# its answer is printed so the log says which browser ran.
- name: Start CloakBrowser
run: |
docker run -d --name cloak --network host \
-e CLOAKBROWSER_AUTO_UPDATE=false \
"$CLOAK_IMAGE" cloakserve
for attempt in $(seq 1 30); do
if curl -fsS "http://$CLOAK_DEBUGGER_ADDRESS/json/version"; then
echo
exit 0
fi
sleep 2
done
echo "CloakBrowser did not answer on $CLOAK_DEBUGGER_ADDRESS"
docker logs cloak
exit 1

- name: Build
run: dotnet build -c Release

# Only the tests that attach to CloakBrowser run here, as the browsers
# job already starts Chrome and Firefox on this image. A Cloak test that
# was skipped would leave this job green having attached to nothing, so
# the pass is looked for by name.
- name: Attach to CloakBrowser
run: |
set -o pipefail
dotnet test -c Release --no-build \
--filter "TestCategory=Browser&Name~Cloak_" \
--logger "console;verbosity=normal" | tee browser-tests.log
grep -q "Passed Cloak_RunsThePage" browser-tests.log

- name: Stop CloakBrowser
if: always()
run: |
docker logs cloak || true
docker rm -f cloak || true
16 changes: 16 additions & 0 deletions BrowserCache/SessionStorageCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,22 @@ public void SessionStorageCache_FireFox(bool enableCookies)
RunTest(driver, enableCookies);
}

/// <summary>
/// Session storage cache behaviour in CloakBrowser.
/// </summary>
[TestMethod, RequiresCloak]
[DataRow(true)]
[DataRow(false)]
public void SessionStorageCache_Cloak(bool enableCookies)
{
var options = new ChromeOptions();
options.AcceptInsecureCertificates = true;

driver = BrowserDrivers.CreateCloak(options);

RunTest(driver, enableCookies);
}

private void RunTest(WebDriver driver, bool enableCookies)
{
ClientServerUrl = $"http://localhost:{TestHelpers.GetRandomUnusedPort()}/";
Expand Down
172 changes: 172 additions & 0 deletions Browsers/CloakTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
using System;
using FiftyOne.Pipeline.Cloud.SeleniumTests.Selenium;
using FiftyOne.Pipeline.Cloud.Tests.Common.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using System.Linq;
using System.Net;
using System.Threading;
using FiftyOne.Pipeline.Cloud.SeleniumTests.Helpers;

namespace FiftyOne.Pipeline.Cloud.SeleniumTests.Browsers
{
/// <summary>
/// Performs the Selenium Tests of <see cref="ChromeTests"/> in
/// CloakBrowser, through a driver attached to the one
/// CLOAK_DEBUGGER_ADDRESS names, and is skipped when it names none.
/// Driver and HTTP server are created once per class for performance.
/// </summary>
/// <remarks>
/// The script these tests run in the browser carries the resource key
/// and the license it was requested with, so both are given to
/// CloakBrowser.
/// </remarks>
[RequiresCloak]
[TestClass, TestCategory("CloudInternal")]
public class CloakTests : SeleniumTestsBase
{
private static WebDriver s_driver;
private static string s_clientServerUrl;
private static HttpListener s_clientServer;
private static CancellationTokenSource s_cts;

/// <summary>
/// Initializes the test class.
/// </summary>
/// <param name="context">The test context.</param>
[ClassInitialize]
public static void ClassInit(TestContext context)
{
// Start the client server once for all tests in this class.
s_clientServerUrl =
$"http://localhost:{TestHelpers.GetRandomUnusedPort()}/";
s_cts = new CancellationTokenSource();
s_clientServer = TestHelpers.SimpleListener(
s_clientServerUrl, s_cts.Token);

var options = new ChromeOptions();
options.AcceptInsecureCertificates = true;
s_driver = BrowserDrivers.CreateCloak(options);
}

/// <summary>
/// Initializes the test.
/// </summary>
[TestInitialize]
public override void Init()
{
base.Init();
Driver = s_driver;
ClientServerUrl = s_clientServerUrl;
Driver.Navigate().GoToUrl(ClientServerUrl);
}

/// <summary>
/// Cleans up after the test class.
/// </summary>
[ClassCleanup]
public static void ClassCleanup()
{
s_driver?.Quit();
s_driver = null;
StopListener(s_clientServer, s_cts);
}

/// <summary>
/// Test expected behavior when request contains valid license.
/// </summary>
[TestMethod]
[DynamicData(
nameof(JavaScript_Get_ValidLicense_DATA),
typeof(SeleniumTestsBase))]
public void JavaScript_Get_ValidLicense(
string endpoint,
string objectName,
string[] values,
string license,
params string[] parameters)
{
var js = TestJavaScript(
endpoint, objectName, values, license, parameters);

AssertEveryValueIsSet(js, objectName, values);
}

/// <summary>
/// Test expected behavior when request does not contain the required
/// license.
/// </summary>
[TestMethod]
[DynamicData(
nameof(JavaScript_Get_NoLicense_DATA),
typeof(SeleniumTestsBase))]
public void JavaScript_Get_NoLicense(
string endpoint,
string[] values,
string license,
params string[] parameters)
{
var js = TestJavaScript(endpoint, "", values, license, parameters);

foreach (var property in values)
{
var parts = property.Split('.');
var element = string.Join('.', parts.Take(parts.Length - 1));
var propertyName = parts.Last();

var propertyExists = js.ExecuteScript(
$"return fod.{element}.hasOwnProperty('{propertyName}');");
Assert.IsTrue((bool)propertyExists);

var value = js.ExecuteScript(
$"return fod.{element}.{propertyName};");
Assert.IsNotNull(
value, $"fod.{element}.{propertyName} is null");

Assert.IsTrue(value.ToString().Contains(
$"{propertyName} is a paid feature.",
StringComparison.OrdinalIgnoreCase));
}
}

/// <summary>
/// Test expected behavior when the request asks for properties that
/// evidence gathered in the browser overrides.
/// </summary>
[TestMethod]
[DynamicData(
nameof(JavaScript_Get_ClientSideOverrides_DATA),
typeof(SeleniumTestsBase))]
public void JavaScript_Get_ClientSideOverrides(
string endpoint,
string objectName,
string[] values,
string license,
params string[] parameters)
{
var js = TestJavaScript(
endpoint, objectName, values, license, parameters);

AssertEveryValueIsSet(js, objectName, values);
}

/// <summary>
/// Checks the object the include made holds a value for every
/// property that was asked for.
/// </summary>
/// <param name="js">Runs script in the page.</param>
/// <param name="objectName">Name of the object in the page.</param>
/// <param name="values">Properties, each as element.property.</param>
private static void AssertEveryValueIsSet(
IJavaScriptExecutor js, string objectName, string[] values)
{
foreach (var property in values)
{
var value = js.ExecuteScript(
$"return {objectName}.{property};");
Assert.IsNotNull(value, $"{objectName}.{property} is null");
}
}
}
}
45 changes: 43 additions & 2 deletions ClientSideOverrides/ExampleClientSideOverridePageContentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,57 @@ public void Example_RendersClientSideOverridesOnThePage(

_driver = BrowserDrivers.CreateChrome(chromeOptions);

AssertRendersScreenSize(width, height);
}

/// <summary>
/// The same in CloakBrowser, with the screen it reports for itself.
/// No device is emulated, as ChromeDriver refuses mobile emulation
/// for a browser it attaches to, so the size to expect is read from
/// the browser.
/// </summary>
[TestMethod, RequiresCloak]
public void Example_RendersClientSideOverridesOnThePage_Cloak()
{
var chromeOptions = new ChromeOptions();
chromeOptions.AcceptInsecureCertificates = true;

_driver = BrowserDrivers.CreateCloak(chromeOptions);

var js = (IJavaScriptExecutor)_driver;
var width = Convert.ToInt64(
js.ExecuteScript("return window.screen.width"));
var height = Convert.ToInt64(
js.ExecuteScript("return window.screen.height"));

// The server-side rows can read 0 for a desktop browser, so a
// screen of 0 would match whether or not the callback ran.
Assert.IsTrue(
width > 0 && height > 0,
$"the browser reports a screen of {width} by {height}, so " +
"the client-side results cannot be told from the server's");

AssertRendersScreenSize(width, height);
}

/// <summary>
/// Loads the example and checks the client-side results it renders
/// against the size of the browser's screen.
/// </summary>
private void AssertRendersScreenSize(long width, long height)
{
_driver.Navigate().GoToUrl(_proxyUrl);

var rendered = WaitForRenderedResults();

Assert.AreEqual(
width.ToString(), rendered[ScreenWidthLabel],
"the screen width rendered on the page does not match the emulated width");
"the screen width rendered on the page does not match the " +
"width of the browser's screen");
Assert.AreEqual(
height.ToString(), rendered[ScreenHeightLabel],
"the screen height rendered on the page does not match the emulated height");
"the screen height rendered on the page does not match the " +
"height of the browser's screen");

// The device id is only rendered by examples whose copy of the
// shared helper is current. An example still carrying an older copy
Expand Down
Loading
Loading