[Blazor][Fixes #13357] input type=time reseting value (#14379)

* [Blazor] input type=time reseting value
* Normalizes values on the client and sends proper dates and times to
  the server.
* Normalizes values applied from the server to the client.
* Introduces @bind and @bind-value support for inputs of types
  datetime-local, month, time.

Fixes #13357

* Update JS
This commit is contained in:
Javier Calvarro Nelson 2019-10-03 00:21:00 +02:00 committed by Artak
parent 6dc49b45c4
commit cca42d9624
12 changed files with 429 additions and 19 deletions

View File

@ -1,4 +1,4 @@
@page "/"
@page "/"
<h1>Hello, world!</h1>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -341,9 +341,18 @@ export class BrowserRenderer {
}
}
private tryApplyValueProperty(batch: RenderBatch, element: Element, attributeFrame: RenderTreeFrame | null) {
private tryApplyValueProperty(batch: RenderBatch, element: Element, attributeFrame: RenderTreeFrame | null): boolean {
// Certain elements have built-in behaviour for their 'value' property
const frameReader = batch.frameReader;
if (element.tagName === 'INPUT' && element.getAttribute('type') === 'time' && !element.getAttribute('step')) {
const timeValue = attributeFrame ? frameReader.attributeValue(attributeFrame) : null;
if (timeValue) {
element['value'] = timeValue.substring(0, 5);
return true;
}
}
switch (element.tagName) {
case 'INPUT':
case 'SELECT':

View File

@ -1,13 +1,19 @@
export class EventForDotNet<TData extends UIEventArgs> {
constructor(public readonly type: EventArgsType, public readonly data: TData) {
public constructor(public readonly type: EventArgsType, public readonly data: TData) {
}
static fromDOMEvent(event: Event): EventForDotNet<UIEventArgs> {
public static fromDOMEvent(event: Event): EventForDotNet<UIEventArgs> {
const element = event.target as Element;
switch (event.type) {
case 'input':
case 'change': {
if (isTimeBasedInput(element)) {
const normalizedValue = normalizeTimeBasedValue(element);
return new EventForDotNet<UIChangeEventArgs>('change', { type: event.type, value: normalizedValue });
}
const targetIsCheckbox = isCheckbox(element);
const newValue = targetIsCheckbox ? !!element['checked'] : element['value'];
return new EventForDotNet<UIChangeEventArgs>('change', { type: event.type, value: newValue });
@ -36,7 +42,7 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'keydown':
case 'keyup':
case 'keypress':
return new EventForDotNet<UIKeyboardEventArgs>('keyboard', parseKeyboardEvent(<KeyboardEvent>event));
return new EventForDotNet<UIKeyboardEventArgs>('keyboard', parseKeyboardEvent(event as KeyboardEvent));
case 'contextmenu':
case 'click':
@ -46,10 +52,10 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'mousedown':
case 'mouseup':
case 'dblclick':
return new EventForDotNet<UIMouseEventArgs>('mouse', parseMouseEvent(<MouseEvent>event));
return new EventForDotNet<UIMouseEventArgs>('mouse', parseMouseEvent(event as MouseEvent));
case 'error':
return new EventForDotNet<UIErrorEventArgs>('error', parseErrorEvent(<ErrorEvent>event));
return new EventForDotNet<UIErrorEventArgs>('error', parseErrorEvent(event as ErrorEvent));
case 'loadstart':
case 'timeout':
@ -57,7 +63,7 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'load':
case 'loadend':
case 'progress':
return new EventForDotNet<UIProgressEventArgs>('progress', parseProgressEvent(<ProgressEvent>event));
return new EventForDotNet<UIProgressEventArgs>('progress', parseProgressEvent(event as ProgressEvent));
case 'touchcancel':
case 'touchend':
@ -65,7 +71,7 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'touchenter':
case 'touchleave':
case 'touchstart':
return new EventForDotNet<UITouchEventArgs>('touch', parseTouchEvent(<TouchEvent>event));
return new EventForDotNet<UITouchEventArgs>('touch', parseTouchEvent(event as TouchEvent));
case 'gotpointercapture':
case 'lostpointercapture':
@ -77,11 +83,11 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'pointerout':
case 'pointerover':
case 'pointerup':
return new EventForDotNet<UIPointerEventArgs>('pointer', parsePointerEvent(<PointerEvent>event));
return new EventForDotNet<UIPointerEventArgs>('pointer', parsePointerEvent(event as PointerEvent));
case 'wheel':
case 'mousewheel':
return new EventForDotNet<UIWheelEventArgs>('wheel', parseWheelEvent(<WheelEvent>event));
return new EventForDotNet<UIWheelEventArgs>('wheel', parseWheelEvent(event as WheelEvent));
default:
return new EventForDotNet<UIEventArgs>('unknown', { type: event.type });
@ -204,8 +210,38 @@ function parseMouseEvent(event: MouseEvent) {
};
}
function isCheckbox(element: Element | null) {
return element && element.tagName === 'INPUT' && element.getAttribute('type') === 'checkbox';
function isCheckbox(element: Element | null): boolean {
return !!element && element.tagName === 'INPUT' && element.getAttribute('type') === 'checkbox';
}
const timeBasedInputs = [
'date',
'datetime-local',
'month',
'time',
'week',
];
function isTimeBasedInput(element: Element): element is HTMLInputElement {
return timeBasedInputs.indexOf(element.getAttribute('type')!) !== -1;
}
function normalizeTimeBasedValue(element: HTMLInputElement): string {
const value = element.value;
const type = element.type;
switch (type) {
case 'date':
case 'datetime-local':
case 'month':
return value;
case 'time':
return value.length === 5 ? value + ':00' : value; // Convert hh:mm to hh:mm:00
case 'week':
// For now we are not going to normalize input type week as it is not trivial
return value;
}
throw new Error(`Invalid element type '${type}'.`);
}
// The following interfaces must be kept in sync with the UIEventArgs C# classes

View File

@ -172,9 +172,15 @@ namespace Microsoft.AspNetCore.Components.Web
[Microsoft.AspNetCore.Components.BindInputElementAttribute("checkbox", null, "checked", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("date", "value", "value", "onchange", true, "yyyy-MM-dd")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("date", null, "value", "onchange", true, "yyyy-MM-dd")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("datetime-local", "value", "value", "onchange", true, "yyyy-MM-ddTHH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("datetime-local", null, "value", "onchange", true, "yyyy-MM-ddTHH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("month", "value", "value", "onchange", true, "yyyy-MM")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("month", null, "value", "onchange", true, "yyyy-MM")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("number", "value", "value", "onchange", true, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("number", null, "value", "onchange", true, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("text", null, "value", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("time", "value", "value", "onchange", true, "HH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("time", null, "value", "onchange", true, "HH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute(null, "value", "value", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute(null, null, "value", "onchange", false, null)]
public static partial class BindAttributes

View File

@ -172,9 +172,15 @@ namespace Microsoft.AspNetCore.Components.Web
[Microsoft.AspNetCore.Components.BindInputElementAttribute("checkbox", null, "checked", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("date", "value", "value", "onchange", true, "yyyy-MM-dd")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("date", null, "value", "onchange", true, "yyyy-MM-dd")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("datetime-local", "value", "value", "onchange", true, "yyyy-MM-ddTHH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("datetime-local", null, "value", "onchange", true, "yyyy-MM-ddTHH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("month", "value", "value", "onchange", true, "yyyy-MM")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("month", null, "value", "onchange", true, "yyyy-MM")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("number", "value", "value", "onchange", true, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("number", null, "value", "onchange", true, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("text", null, "value", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("time", "value", "value", "onchange", true, "HH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("time", null, "value", "onchange", true, "HH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute(null, "value", "value", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute(null, null, "value", "onchange", false, null)]
public static partial class BindAttributes

View File

@ -30,6 +30,20 @@ namespace Microsoft.AspNetCore.Components.Web
[BindInputElement("date", null, "value", "onchange", isInvariantCulture: true, format: "yyyy-MM-dd")]
[BindInputElement("date", "value", "value", "onchange", isInvariantCulture: true, format: "yyyy-MM-dd")]
// type="datetime-local" is invariant culture with a specific format.
// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings for details.
[BindInputElement("datetime-local", null, "value", "onchange", isInvariantCulture: true, format: "yyyy-MM-ddTHH:mm:ss")]
[BindInputElement("datetime-local", "value", "value", "onchange", isInvariantCulture: true, format: "yyyy-MM-ddTHH:mm:ss")]
// type="month" is invariant culture with a specific format.
// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings for details.
[BindInputElement("month", null, "value", "onchange", isInvariantCulture: true, format: "yyyy-MM")]
[BindInputElement("month", "value", "value", "onchange", isInvariantCulture: true, format: "yyyy-MM")]
// type="time" is invariant culture with a specific format.
[BindInputElement("time", null, "value", "onchange", isInvariantCulture: true, format: "HH:mm:ss")]
[BindInputElement("time", "value", "value", "onchange", isInvariantCulture: true, format: "HH:mm:ss")]
[BindElement("select", null, "value", "onchange")]
[BindElement("textarea", null, "value", "onchange")]
public static class BindAttributes

View File

@ -2,10 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text.Json;
using BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using Moq;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Xunit;
@ -1018,5 +1020,263 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal(expected.DateTime, () => DateTimeOffset.Parse(boundValue.Text).DateTime);
Assert.Equal(expected.DateTime, DateTimeOffset.Parse(mirrorValue.GetAttribute("value")).DateTime);
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindDateTimeLocalTextboxDateTime()
{
var target = Browser.FindElement(By.Id("datetime-local-textbox-datetime"));
var boundValue = Browser.FindElement(By.Id("datetime-local-textbox-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("datetime-local-textbox-datetime-mirror"));
var expected = new DateTime(1985, 3, 4);
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Clear textbox; value updates to 01/01/0001 because that's the default
target.Clear();
expected = default;
Browser.Equal(expected, () => DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#datetime-local-textbox-datetime", "2000-01-02T04:05:06");
expected = new DateTime(2000, 1, 2, 04, 05, 06);
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindDateTimeLocalTextboxNullableDateTime()
{
var target = Browser.FindElement(By.Id("datetime-local-textbox-nullable-datetime"));
var boundValue = Browser.FindElement(By.Id("datetime-local-textbox-nullable-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("datetime-local-textbox-nullable-datetime-mirror"));
Assert.Equal(string.Empty, target.GetAttribute("value"));
Assert.Equal(string.Empty, boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
Browser.Equal("", () => boundValue.Text);
Assert.Equal("", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#datetime-local-textbox-nullable-datetime", "2000-01-02T04:05:06");
var expected = new DateTime(2000, 1, 2, 04, 05, 06);
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindMonthTextboxDateTime()
{
var target = Browser.FindElement(By.Id("month-textbox-datetime"));
var boundValue = Browser.FindElement(By.Id("month-textbox-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("month-textbox-datetime-mirror"));
var expected = new DateTime(1985, 3, 1);
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value")));
// When the value gets displayed the first time it gets truncated to the 1st day,
// until there is no change the bound value doesn't get updated.
Assert.Equal(expected.AddDays(3), DateTime.Parse(boundValue.Text));
Assert.Equal(expected.AddDays(3), DateTime.Parse(mirrorValue.GetAttribute("value")));
// Clear textbox; value updates to 01/01/0001 because that's the default
target.Clear();
expected = default;
Browser.Equal(expected, () => DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#month-textbox-datetime", "2000-02");
expected = new DateTime(2000, 2, 1);
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindMonthTextboxNullableDateTime()
{
var target = Browser.FindElement(By.Id("month-textbox-nullable-datetime"));
var boundValue = Browser.FindElement(By.Id("month-textbox-nullable-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("month-textbox-nullable-datetime-mirror"));
Assert.Equal(string.Empty, target.GetAttribute("value"));
Assert.Equal(string.Empty, boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
Browser.Equal("", () => boundValue.Text);
Assert.Equal("", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#month-textbox-nullable-datetime", "2000-02");
var expected = new DateTime(2000, 2, 1);
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindTimeTextboxDateTime()
{
var target = Browser.FindElement(By.Id("time-textbox-datetime"));
var boundValue = Browser.FindElement(By.Id("time-textbox-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("time-textbox-datetime-mirror"));
var expected = DateTime.Now.Date.AddHours(8).AddMinutes(5);
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Clear textbox; value updates to 00:00 because that's the default
target.Clear();
expected = default;
Browser.Equal(DateTime.Now.Date, () => DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(default, DateTime.Parse(boundValue.Text));
Assert.Equal(default, DateTime.Parse(mirrorValue.GetAttribute("value")));
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#time-textbox-datetime", "04:05");
expected = DateTime.Now.Date.Add(new TimeSpan(4, 5, 0));
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindTimeTextboxNullableDateTime()
{
var target = Browser.FindElement(By.Id("time-textbox-nullable-datetime"));
var boundValue = Browser.FindElement(By.Id("time-textbox-nullable-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("time-textbox-nullable-datetime-mirror"));
Assert.Equal(string.Empty, target.GetAttribute("value"));
Assert.Equal(string.Empty, boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
Browser.Equal("", () => boundValue.Text);
Assert.Equal("", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#time-textbox-nullable-datetime", "05:06");
var expected = DateTime.Now.Date.Add(new TimeSpan(05, 06, 0));
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindTimeStepTextboxDateTime()
{
var target = Browser.FindElement(By.Id("time-step-textbox-datetime"));
var boundValue = Browser.FindElement(By.Id("time-step-textbox-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("time-step-textbox-datetime-mirror"));
var expected = DateTime.Now.Date.Add(new TimeSpan(8, 5, 30));
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Clear textbox; value updates to 00:00 because that's the default
target.Clear();
expected = default;
Browser.Equal(DateTime.Now.Date, () => DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(default, DateTime.Parse(boundValue.Text));
Assert.Equal(default, DateTime.Parse(mirrorValue.GetAttribute("value")));
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#time-step-textbox-datetime", "04:05:06");
expected = DateTime.Now.Date.Add(new TimeSpan(4, 5, 6));
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindTimeStepTextboxNullableDateTime()
{
var target = Browser.FindElement(By.Id("time-step-textbox-nullable-datetime"));
var boundValue = Browser.FindElement(By.Id("time-step-textbox-nullable-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("time-step-textbox-nullable-datetime-mirror"));
Assert.Equal(string.Empty, target.GetAttribute("value"));
Assert.Equal(string.Empty, boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
Browser.Equal("", () => boundValue.Text);
Assert.Equal("", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#time-step-textbox-nullable-datetime", "05:06");
var expected = DateTime.Now.Date.Add(new TimeSpan(05, 06, 0));
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
// Applies an input through javascript to datetime-local/month/time controls.
private void ApplyInputValue(string cssSelector, string value)
{
// It's very difficult to enter an invalid value into an <input type=date>, because
// most combinations of keystrokes get normalized to something valid. Additionally,
// using Selenium's SendKeys interacts unpredictably with this normalization logic,
// most likely based on timings. As a workaround, use JS to apply the values. This
// should only be used when strictly necessary, as it doesn't represent actual user
// interaction as authentically as SendKeys in other cases.
var javascript = (IJavaScriptExecutor)Browser;
javascript.ExecuteScript(
$"var elem = document.querySelector('{cssSelector}');"
+ $"elem.value = '{value}';"
+ "elem.dispatchEvent(new KeyboardEvent('change'));");
}
}
}

View File

@ -254,6 +254,63 @@
<span id="select-markup-box-value">@selectMarkupValue</span>
</p>
<h2>Time related inputs (datetime-local, month, time)</h2>
<h3>datetime-local</h3>
<p>
DateTime:
<input id="datetime-local-textbox-datetime" @bind="dateTimeLocalTextboxDateTimeValue" type="datetime-local" />
<span id="datetime-local-textbox-datetime-value">@dateTimeLocalTextboxDateTimeValue</span>
<input id="datetime-local-textbox-datetime-mirror" @bind="dateTimeLocalTextboxDateTimeValue" readonly />
</p>
<p>
Nullable DateTime:
<input id="datetime-local-textbox-nullable-datetime" @bind-value="dateTimeLocalTextboxNullableDateTimeValue" type="datetime-local" />
<span id="datetime-local-textbox-nullable-datetime-value">@dateTimeLocalTextboxNullableDateTimeValue</span>
<input id="datetime-local-textbox-nullable-datetime-mirror" @bind="dateTimeLocalTextboxNullableDateTimeValue" readonly />
</p>
<h3>month</h3>
<p>
DateTime:
<input id="month-textbox-datetime" @bind="monthTextboxDateTimeValue" type="month" />
<span id="month-textbox-datetime-value">@monthTextboxDateTimeValue</span>
<input id="month-textbox-datetime-mirror" @bind="monthTextboxDateTimeValue" readonly />
</p>
<p>
Nullable DateTime:
<input id="month-textbox-nullable-datetime" @bind-value="monthTextboxNullableDateTimeValue" type="month" />
<span id="month-textbox-nullable-datetime-value">@monthTextboxNullableDateTimeValue</span>
<input id="month-textbox-nullable-datetime-mirror" @bind="monthTextboxNullableDateTimeValue" readonly />
</p>
<h3>time</h3>
<p>
DateTime:
<input id="time-textbox-datetime" @bind="timeTextboxDateTimeValue" type="time" />
<span id="time-textbox-datetime-value">@timeTextboxDateTimeValue</span>
<input id="time-textbox-datetime-mirror" @bind="timeTextboxDateTimeValue" readonly />
</p>
<p>
Nullable DateTime:
<input id="time-textbox-nullable-datetime" @bind-value="timeTextboxNullableDateTimeValue" type="time" />
<span id="time-textbox-nullable-datetime-value">@timeTextboxNullableDateTimeValue</span>
<input id="time-textbox-nullable-datetime-mirror" @bind="timeTextboxNullableDateTimeValue" readonly />
</p>
<h3>time with step (supports seconds)</h3>
<p>
DateTime:
<input id="time-step-textbox-datetime" @bind="timeStepTextboxDateTimeValue" type="time" step="1" />
<span id="time-step-textbox-datetime-value">@timeStepTextboxDateTimeValue</span>
<input id="time-step-textbox-datetime-mirror" @bind="timeStepTextboxDateTimeValue" readonly />
</p>
<p>
Nullable DateTime:
<input id="time-step-textbox-nullable-datetime" @bind-value="timeStepTextboxNullableDateTimeValue" type="time" step="1" />
<span id="time-step-textbox-nullable-datetime-value">@timeStepTextboxNullableDateTimeValue</span>
<input id="time-step-textbox-nullable-datetime-mirror" @bind="timeStepTextboxNullableDateTimeValue" readonly />
</p>
@code {
string textboxInitiallyBlankValue = null;
string textboxInitiallyPopulatedValue = "Hello";
@ -297,6 +354,18 @@
DateTime textboxDateTimeFormatInvalidValue = new DateTime(1985, 3, 4);
DateTimeOffset? textboxNullableDateTimeOffsetFormatInvalidValue = null;
DateTime dateTimeLocalTextboxDateTimeValue = new DateTime(1985, 3, 4);
DateTime? dateTimeLocalTextboxNullableDateTimeValue = null;
DateTime monthTextboxDateTimeValue = new DateTime(1985, 3, 4);
DateTime? monthTextboxNullableDateTimeValue = null;
DateTime timeTextboxDateTimeValue = DateTime.Now.Date.Add(new TimeSpan(8, 5, 0));
DateTime? timeTextboxNullableDateTimeValue = null;
DateTime timeStepTextboxDateTimeValue = DateTime.Now.Date.Add(new TimeSpan(8, 5, 30));
DateTime? timeStepTextboxNullableDateTimeValue = null;
bool includeFourthOption = false;
enum SelectableValue { First, Second, Third, Fourth }
SelectableValue selectValue = SelectableValue.Second;

View File

@ -103,4 +103,13 @@
DateTime inputDateDateTime = new DateTime(1985, 3, 4);
DateTimeOffset inputDateDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4));
DateTime inputTypeDateTimeLocalDateTime = new DateTime(1985, 3, 4);
DateTimeOffset inputTypeDateTimeLocalDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4));
DateTime inputTypeMonthDateTime = new DateTime(1985, 3, 4);
DateTimeOffset inputTypeMonthDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4));
DateTime inputTypeTimeDateTime = new DateTime(1985, 3, 4);
DateTimeOffset inputTypeTimeDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4));
}

View File

@ -14,13 +14,14 @@ namespace OpenQA.Selenium
public class BrowserAssertFailedException : XunitException
{
public BrowserAssertFailedException(IReadOnlyList<LogEntry> logs, Exception innerException, string screenShotPath)
: base(BuildMessage(logs, screenShotPath), innerException)
: base(BuildMessage(innerException, logs, screenShotPath), innerException)
{
}
private static string BuildMessage(IReadOnlyList<LogEntry> logs, string screenShotPath) =>
private static string BuildMessage(Exception innerException, IReadOnlyList<LogEntry> logs, string screenShotPath) =>
innerException.ToString() + Environment.NewLine +
(File.Exists(screenShotPath) ? $"Screen shot captured at '{screenShotPath}'" + Environment.NewLine : "") +
(logs.Count > 0 ? "Encountered browser errors" : "No browser errors found") + " while running the assertion." + Environment.NewLine +
(logs.Count > 0 ? "Encountered browser logs" : "No browser logs found") + " while running the assertion." + Environment.NewLine +
string.Join(Environment.NewLine, logs);
}
}