Skip to content

Commit 89e642a

Browse files
committed
Resolve #250 - Add tests for entering dates
1 parent 05862e0 commit 89e642a

5 files changed

Lines changed: 250 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System;
2+
using System.Globalization;
3+
using CSF.Screenplay.Performables;
4+
using CSF.Screenplay.Selenium.Actions;
5+
using CSF.Screenplay.Selenium.Elements;
6+
7+
namespace CSF.Screenplay.Selenium.Builders
8+
{
9+
/// <summary>
10+
/// A builder type which creates an instance of <see cref="EnterTheDate"/>.
11+
/// </summary>
12+
public class EnterTheDateBuilder : IGetsPerformable
13+
{
14+
readonly DateTime? date;
15+
ITarget target;
16+
CultureInfo culture;
17+
18+
/// <summary>
19+
/// Specifies the target element into which to enter the date. This must be an <c>&lt;input type="date"&gt;</c> element.
20+
/// </summary>
21+
/// <param name="target">The target element</param>
22+
/// <returns>This same builder, so calls may be chained</returns>
23+
/// <exception cref="ArgumentNullException">If <paramref name="target"/> is null</exception>
24+
/// <exception cref="InvalidOperationException">If this method is used more than once</exception>
25+
public EnterTheDateBuilder Into(ITarget target)
26+
{
27+
if (target is null)
28+
throw new ArgumentNullException(nameof(target));
29+
if(this.target != null)
30+
throw new InvalidOperationException("The target has already been set; it may not be set again.");
31+
32+
this.target = target;
33+
return this;
34+
}
35+
36+
/// <summary>
37+
/// Specifies the culture for which to enter the date. This must be the culture in which the web browser is operating.
38+
/// </summary>
39+
/// <remarks>
40+
/// <para>
41+
/// Web browser are culture-aware applications and they will render the input/display value of a date field using the culture
42+
/// in which their operating system is configured. This impacts the manner in which users input dates.
43+
/// If this method is not used, the task returned by this builder will use the culture of the operating system/environment
44+
/// that is executing the Screenplay Performance. This is usually OK when running the web browser locally, but it might not match
45+
/// the browser's culture when using remote web browsers.
46+
/// </para>
47+
/// </remarks>
48+
/// <example>
49+
/// <para>
50+
/// For example, a British English browser <c>en-GB</c> expects dates to be entered in the format ddMMyyyy.
51+
/// However, a US English browser <c>en-US</c> expects dates to be entered in the format MMddyyyy.
52+
/// </para>
53+
/// <para>
54+
/// The <paramref name="cultureIdentifier"/> parameter of this method must be the culture identifier of the culture which the
55+
/// browser is operating under, such as <c>en-GB</c>.
56+
/// </para>
57+
/// </example>
58+
/// <param name="cultureIdentifier">A culture identifier string</param>
59+
/// <returns>This same builder, so calls may be chained</returns>
60+
/// <exception cref="ArgumentNullException">If <paramref name="cultureIdentifier"/> is null</exception>
61+
/// <exception cref="CultureNotFoundException">If <paramref name="cultureIdentifier"/> indicates a culture which is not found</exception>
62+
public EnterTheDateBuilder ForTheCultureNamed(string cultureIdentifier)
63+
{
64+
culture = CultureInfo.GetCultureInfo(cultureIdentifier);
65+
return this;
66+
}
67+
68+
/// <inheritdoc/>
69+
public IPerformable GetPerformable() => new EnterTheDate(date, target, culture);
70+
71+
/// <summary>
72+
/// Initializes a new instance of the <see cref="EnterTheDateBuilder"/> class with the specified date.
73+
/// </summary>
74+
/// <param name="date">The date to enter, or null</param>
75+
public EnterTheDateBuilder(DateTime? date)
76+
{
77+
this.date = date;
78+
}
79+
}
80+
}

CSF.Screenplay.Selenium/PerformableBuilder.elementPerformables.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using CSF.Screenplay.Selenium.Actions;
23
using CSF.Screenplay.Selenium.Builders;
34
using CSF.Screenplay.Selenium.Elements;
@@ -32,6 +33,18 @@ public static partial class PerformableBuilder
3233
/// <returns>A builder with which the user may select a target element.</returns>
3334
public static SendKeysBuilder EnterTheText(params string[] text) => new SendKeysBuilder(string.Join(string.Empty, text));
3435

36+
/// <summary>
37+
/// Gets a builder for creating a performable action which represents an actor entering a date into an <c>&lt;input type="date"&gt;</c> element.
38+
/// </summary>
39+
/// <remarks>
40+
/// <para>
41+
/// If the specified <paramref name="date"/> is <see langword="null"/> then the input element will be cleared.
42+
/// </para>
43+
/// </remarks>
44+
/// <param name="date">The date to enter into the input control.</param>
45+
/// <returns>A builder with which the user may select a target element and optionally a culture.</returns>
46+
public static EnterTheDateBuilder EnterTheDate(DateTime? date) => new EnterTheDateBuilder(date);
47+
3548
/// <summary>
3649
/// Gets a performable which represents an actor deselecting everything from a <c>&lt;select&gt;</c> element.
3750
/// </summary>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System;
2+
using System.Globalization;
3+
using System.Text.RegularExpressions;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using CSF.Screenplay.Selenium.Elements;
7+
using static CSF.Screenplay.Selenium.PerformableBuilder;
8+
9+
namespace CSF.Screenplay.Selenium.Actions
10+
{
11+
/// <summary>
12+
/// A <see cref="IPerformable"/> which represents an actor entering a date value into an <c>&lt;input type="date"&gt;</c> element.
13+
/// </summary>
14+
/// <remarks>
15+
/// <para>
16+
/// Note that this task is culture-sensitive. Ensure that the date value is entered into the browser using the culture in which the browser is
17+
/// running.
18+
/// If no culture information is specified then this task defaults to the current culture: <see cref="CultureInfo.CurrentCulture"/>.
19+
/// However, this is not certain to be correct, particularly in remote/cloud configurations where the web browser is operating on different
20+
/// infrastructure to the computer which is executing the Screenplay performance. These two computers might be operating in different cultures.
21+
/// </para>
22+
/// <para>
23+
/// If the date specified to this task is <see langword="null"/> then this task will clear the date from the target.
24+
/// </para>
25+
/// </remarks>
26+
/// <example>
27+
/// <para>
28+
/// For example, a British English browser <c>en-GB</c> expects dates to be entered in the format ddMMyyyy.
29+
/// However, a US English browser <c>en-US</c> expects dates to be entered in the format MMddyyyy.
30+
/// </para>
31+
/// </example>
32+
public class EnterTheDate : IPerformable, ICanReport
33+
{
34+
const string nonNumericPattern = @"\D";
35+
static readonly Regex nonNumeric = new Regex(nonNumericPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
36+
37+
readonly DateTime? date;
38+
readonly ITarget target;
39+
readonly CultureInfo culture;
40+
41+
string GetShortDatePattern() => culture.DateTimeFormat.ShortDatePattern;
42+
43+
string FormatDate() => date.HasValue ? date.Value.ToString(GetShortDatePattern()) : null;
44+
45+
/// <inheritdoc/>
46+
public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default)
47+
{
48+
if(!date.HasValue)
49+
return actor.PerformAsync(ClearTheContentsOf(target), cancellationToken);
50+
51+
var dateText = nonNumeric.Replace(FormatDate(), string.Empty);
52+
return actor.PerformAsync(EnterTheText(dateText).Into(target), cancellationToken);
53+
}
54+
55+
/// <inheritdoc/>
56+
public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter)
57+
{
58+
return date.HasValue
59+
? formatter.Format("{Actor} enters the date {Date} into {Target}", actor.Name, FormatDate(), target)
60+
: formatter.Format("{Actor} clears the date from {Target}", actor.Name, date, target);
61+
}
62+
63+
/// <summary>
64+
/// Initializes a new instance of the <see cref="EnterTheDate"/> class with the specified date.
65+
/// </summary>
66+
/// <param name="date">The date to enter into the element.</param>
67+
/// <param name="target">The element into which to enter the data</param>
68+
/// <param name="culture">The culture for which to enter the date</param>
69+
public EnterTheDate(DateTime? date, ITarget target, CultureInfo culture = null)
70+
{
71+
this.date = date;
72+
this.target = target ?? throw new ArgumentNullException(nameof(target));
73+
this.culture = culture ?? CultureInfo.CurrentCulture;
74+
}
75+
}
76+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<html>
2+
<head>
3+
</head>
4+
<body>
5+
<h1>Date input Tests</h1>
6+
<p>
7+
Typing into the input box will immediately update the value of the display area, below.
8+
</p>
9+
<input type="date" id="inputArea" value="2001-01-01">
10+
<code><pre id="display" style="background: yellow"></pre></code>
11+
<script>
12+
window.onload = function() {
13+
const inputArea = document.getElementById("inputArea");
14+
inputArea.addEventListener("input", function() {
15+
const displayElement = document.getElementById("display");
16+
displayElement.innerText = inputArea.value;
17+
});
18+
}
19+
</script>
20+
</body>
21+
</html>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System;
2+
using System.Globalization;
3+
using CSF.Screenplay.Selenium.Elements;
4+
using static CSF.Screenplay.PerformanceStarter;
5+
using static CSF.Screenplay.Selenium.PerformableBuilder;
6+
7+
namespace CSF.Screenplay.Selenium.Tasks;
8+
9+
[TestFixture, Parallelizable]
10+
public class EnterTheDateTests
11+
{
12+
static readonly ITarget
13+
inputArea = new ElementId("inputArea", "the input area"),
14+
displayText = new ElementId("display", "the displayable text");
15+
16+
static readonly NamedUri testPage = new NamedUri("InputDateTests.html", "the test page");
17+
18+
[Test, Screenplay]
19+
public async Task EnteringADateShouldYieldTheCorrectValue(IStage stage)
20+
{
21+
var webster = stage.Spotlight<Webster>();
22+
23+
await Given(webster).WasAbleTo(OpenTheUrl(testPage));
24+
await When(webster).AttemptsTo(EnterTheDate(new DateTime(2025, 11, 12)).Into(inputArea));
25+
var result = await Then(webster).Should(ReadFromTheElement(displayText).TheText());
26+
27+
Assert.That(result, Is.EqualTo("2025-11-12"));
28+
}
29+
30+
[Test, Screenplay]
31+
public async Task EnteringANullDateShouldClearTheValue(IStage stage)
32+
{
33+
var webster = stage.Spotlight<Webster>();
34+
35+
await Given(webster).WasAbleTo(OpenTheUrl(testPage));
36+
await When(webster).AttemptsTo(EnterTheDate(null).Into(inputArea));
37+
var result = await Then(webster).Should(ReadFromTheElement(displayText).TheText());
38+
39+
Assert.That(result, Is.EqualTo(string.Empty));
40+
}
41+
42+
[Test, Screenplay]
43+
public async Task EnteringADateInAnUnusualCultureShouldYieldIncorrectResults(IStage stage)
44+
{
45+
var webster = stage.Spotlight<Webster>();
46+
47+
if(CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern.StartsWith("y", StringComparison.InvariantCultureIgnoreCase))
48+
Assert.Inconclusive("This test can't be meaningfully run when the current culture uses Y/M/D date formatting");
49+
50+
await Given(webster).WasAbleTo(OpenTheUrl(testPage));
51+
await When(webster).AttemptsTo(EnterTheDate(new DateTime(2025, 11, 12)).Into(inputArea).ForTheCultureNamed("ja-JP"));
52+
var result = await Then(webster).Should(ReadFromTheElement(displayText).TheText());
53+
54+
Assert.Multiple(() =>
55+
{
56+
Assert.That(result, Is.Not.EqualTo(string.Empty), "The date shouldn't be empty");
57+
Assert.That(result, Is.Not.EqualTo("2025-11-12"), "The date shouldn't be the value which was entered either, because of the culture/format difference");
58+
});
59+
}
60+
}

0 commit comments

Comments
 (0)