-
Notifications
You must be signed in to change notification settings - Fork 97
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
validation client-side: Fix Required(AllowEmptyString=true)
- Loading branch information
Showing
5 changed files
with
335 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
src/Samples/Common/ViewModels/FeatureSamples/Validation/ClientSideRulesViewModel.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.ComponentModel.DataAnnotations; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using DotVVM.Framework.ViewModel; | ||
|
||
namespace DotVVM.Samples.BasicSamples.ViewModels.FeatureSamples.Validation | ||
{ | ||
public class ClientSideRulesViewModel : DotvvmViewModelBase | ||
{ | ||
[Range(10, 20)] | ||
public int? RangeInt32 { get; set; } = null; | ||
|
||
[Range(12.345678901, double.PositiveInfinity)] | ||
public double? RangeFloat64 { get; set; } = null; | ||
|
||
[Range(typeof(DateOnly), "2015-01-01", "2015-12-31")] | ||
public DateOnly? RangeDate { get; set; } = null; | ||
|
||
[Required(AllowEmptyStrings = false)] | ||
public string RequiredString { get; set; } = "abc"; | ||
|
||
[Required(AllowEmptyStrings = true)] | ||
public string NotNullString { get; set; } = ""; | ||
|
||
[EmailAddress] | ||
public string EmailString { get; set; } = "[email protected]"; | ||
|
||
public string Result { get; set; } | ||
[Bind(Direction.ServerToClientFirstRequest)] | ||
public int ServerRequestCount { get; set; } | ||
[Bind(Direction.ServerToClientFirstRequest)] | ||
public int ClientPostbackCount { get; set; } | ||
|
||
public void Command() | ||
{ | ||
Result = "Valid"; | ||
} | ||
|
||
} | ||
} |
87 changes: 87 additions & 0 deletions
87
src/Samples/Common/Views/FeatureSamples/Validation/ClientSideRules.dothtml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
@viewModel DotVVM.Samples.BasicSamples.ViewModels.FeatureSamples.Validation.ClientSideRulesViewModel | ||
|
||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8" /> | ||
<title></title> | ||
</head> | ||
<body> | ||
<table> | ||
<tr> | ||
<td>Int32 Range(10, 20)</td> | ||
<td> | ||
<dot:TextBox Text={value: RangeInt32} Type=number data-ui="textbox-RangeInt32" /> | ||
<dot:Button Click={staticCommand: RangeInt32 = null} data-ui="setnull-RangeInt32">Set null</dot:Button> | ||
</td> | ||
</tr> | ||
<tr> | ||
<td>Float64 Range(12.345678901, Inf)</td> | ||
<td> | ||
<dot:TextBox Text={value: RangeFloat64} data-ui="textbox-RangeFloat64" /> | ||
<dot:Button Click={staticCommand: RangeFloat64 = null} data-ui="setnull-RangeFloat64">Set null</dot:Button> | ||
</td> | ||
</tr> | ||
<tr> | ||
<td>Date Range in 2015</td> | ||
<td> | ||
<dot:TextBox Text={value: RangeDate} Type=date /> | ||
<dot:TextBox Text={value: RangeDate} FormatString="yyyy-MM-dd" data-ui="textbox-RangeDate" /> | ||
<dot:Button Click={staticCommand: RangeDate = null} data-ui="setnull-RangeDate">Set null</dot:Button> | ||
</td> | ||
<tr> | ||
<td>String Required</td> | ||
<td> | ||
<dot:TextBox Text={value: RequiredString} data-ui="textbox-RequiredString" /> | ||
<dot:Button Click={staticCommand: RequiredString = null} data-ui="setnull-RequiredString">Set null</dot:Button> | ||
</td> | ||
</tr> | ||
<tr> | ||
<td>String NotNull</td> | ||
<td> | ||
<dot:TextBox Text={value: NotNullString} data-ui="textbox-NotNullString" /> | ||
<dot:Button Click={staticCommand: NotNullString = null} data-ui="setnull-NotNullString">Set null</dot:Button> | ||
</td> | ||
</tr> | ||
<tr> | ||
<td>String Email</td> | ||
<td> | ||
<dot:TextBox Text={value: EmailString} data-ui="textbox-EmailString" /> | ||
<dot:Button Click={staticCommand: EmailString = null} data-ui="setnull-EmailString">Set null</dot:Button> | ||
</td> | ||
</tr> | ||
</table> | ||
<hr> | ||
<dot:ValidationSummary data-ui="errors" /> | ||
<p> | ||
Server requests: <span data-ui="request-count" InnerText={value: ServerRequestCount} />, | ||
Logical postbacks: <span data-ui="postback-count" InnerText={value: ClientPostbackCount} /> | ||
</p> | ||
<p> | ||
Result: <span data-ui="result" InnerText={value: Result} /> | ||
</p> | ||
<p> | ||
<dot:Button Click={command: Command()} onclick="dotvvm.validation.events.validationErrorsChanged.unsubscribe(validationSupressor)" Text="Submit" data-ui="submit-button" /> | ||
<dot:Button Click={command: Command()} onclick="dotvvm.validation.events.validationErrorsChanged.subscribeOnce(validationSupressor)" Text="Submit without client-side validation" data-ui="submit-button-serverside" /> | ||
<dot:Button Click={command: 0} Validation.Target={value: 0} Text="Clear errors" data-ui="clear-button" /> | ||
</p> | ||
|
||
<dot:InlineScript> | ||
dotvvm.events.postbackHandlersStarted.subscribe(() => { | ||
dotvvm.patchState({ Result: "" }) | ||
}) | ||
dotvvm.events.postbackCommitInvoked.subscribe(() => { | ||
dotvvm.patchState({ ServerRequestCount: dotvvm.state.ServerRequestCount + 1 }) | ||
}) | ||
dotvvm.events.afterPostback.subscribe(() => { | ||
dotvvm.patchState({ ClientPostbackCount: dotvvm.state.ClientPostbackCount + 1 }) | ||
}) | ||
const validationSupressor = () => { | ||
for (const e of [...dotvvm.validation.errors]) { | ||
e.detach() | ||
} | ||
} | ||
</dot:InlineScript> | ||
</body> | ||
</html> | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
203 changes: 203 additions & 0 deletions
203
src/Samples/Tests/Tests/Feature/ValidationClientSideRulesTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
using System; | ||
using System.Globalization; | ||
using System.Linq; | ||
using DotVVM.Samples.Tests.Base; | ||
using DotVVM.Testing.Abstractions; | ||
using OpenQA.Selenium; | ||
using Riganti.Selenium.Core; | ||
using Riganti.Selenium.Core.Abstractions; | ||
using Riganti.Selenium.DotVVM; | ||
using Xunit; | ||
using Xunit.Abstractions; | ||
|
||
namespace DotVVM.Samples.Tests.Feature | ||
{ | ||
public class ValidationClientSideRulesTests : AppSeleniumTest | ||
{ | ||
public ValidationClientSideRulesTests(ITestOutputHelper output) : base(output) | ||
{ | ||
} | ||
|
||
(int requests, int postbacks) PostbacksCounts(IBrowserWrapper browser) | ||
{ | ||
var requestCount = browser.Single("request-count", SelectByDataUi).GetInnerText(); | ||
var postbackCount = browser.Single("postback-count", SelectByDataUi).GetInnerText(); | ||
|
||
return (int.Parse(requestCount), int.Parse(postbackCount)); | ||
} | ||
void ExpectNoErrors(IBrowserWrapper browser) | ||
{ | ||
var count = PostbacksCounts(browser); | ||
browser.Single("submit-button", SelectByDataUi).Click(); | ||
try | ||
{ | ||
AssertUI.InnerTextEquals(browser.Single("postback-count", SelectByDataUi), (count.postbacks + 1).ToString()); | ||
AssertUI.InnerTextEquals(browser.Single("request-count", SelectByDataUi), (count.requests + 1).ToString()); | ||
AssertUI.InnerTextEquals(browser.Single("result", SelectByDataUi), "Valid"); | ||
Assert.Empty(browser.FindElements("ul[data-ui=errors] > li")); | ||
} | ||
catch (Exception e) | ||
{ | ||
var errors = browser.FindElements("ul[data-ui=errors] > li").Select(t => t.GetInnerText()).ToArray(); | ||
if (errors.Length > 0) | ||
{ | ||
throw new Exception($"Validation failed with errors: {string.Join(", ", errors)}", e); | ||
} | ||
throw; | ||
} | ||
} | ||
|
||
void ExpectErrors(IBrowserWrapper browser, string[] expectedErrors) | ||
{ | ||
var count = PostbacksCounts(browser); | ||
// client-side validation | ||
browser.Single("submit-button", SelectByDataUi).Click(); | ||
AssertUI.InnerTextEquals(browser.Single("postback-count", SelectByDataUi), (count.postbacks + 1).ToString()); | ||
var errors = browser.WaitFor(_ => { | ||
var c = browser.FindElements("ul[data-ui=errors] > li"); | ||
Assert.NotEmpty(c); | ||
return c; | ||
}).Select(t => t.GetInnerText()).ToArray(); | ||
AssertUI.InnerTextEquals(browser.Single("request-count", SelectByDataUi), count.requests.ToString(), failureMessage: $"Validation didn't run client-side (got errors: {string.Join(", ", errors)}."); | ||
AssertUI.InnerTextEquals(browser.Single("result", SelectByDataUi), ""); | ||
Assert.Equal(expectedErrors.OrderBy(t => t).ToArray(), errors.OrderBy(t => t).ToArray()); | ||
|
||
// server-side validation | ||
browser.Single("submit-button-serverside", SelectByDataUi).Click(); | ||
AssertUI.InnerTextEquals(browser.Single("postback-count", SelectByDataUi), (count.postbacks + 2).ToString()); | ||
AssertUI.InnerTextEquals(browser.Single("request-count", SelectByDataUi), (count.requests + 1).ToString()); | ||
AssertUI.InnerTextEquals(browser.Single("result", SelectByDataUi), ""); | ||
errors = browser.WaitFor(_ => { | ||
var c = browser.FindElements("ul[data-ui=errors] > li"); | ||
Assert.NotEmpty(c); | ||
return c; | ||
}).Select(t => t.GetInnerText()).ToArray(); | ||
Assert.Equal(expectedErrors.OrderBy(t => t).ToArray(), errors.OrderBy(t => t).ToArray()); | ||
} | ||
|
||
void SetValue(IBrowserWrapper browser, string property, string value) | ||
{ | ||
if (value == null) | ||
browser.Single($"setnull-{property}", SelectByDataUi).Click(); | ||
else | ||
browser.Single($"textbox-{property}", SelectByDataUi).Clear().SendKeys(value); | ||
} | ||
|
||
[Theory] | ||
[InlineData("10", null)] | ||
[InlineData("20", null)] | ||
[InlineData("", null)] | ||
[InlineData(null, null)] | ||
[InlineData("-1", "The field RangeInt32 must be between 10 and 20.")] | ||
[InlineData("0", "The field RangeInt32 must be between 10 and 20.")] | ||
public void Feature_Validation_ClienSideRules_RangeInt32(string value, string error) | ||
{ | ||
RunInAllBrowsers(browser => { | ||
browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_Validation_ClientSideRules); | ||
|
||
SetValue(browser, "RangeInt32", value); | ||
if (error == null) | ||
ExpectNoErrors(browser); | ||
else | ||
ExpectErrors(browser, new[] { error }); | ||
}); | ||
} | ||
|
||
[Theory] | ||
[InlineData("12.345678901", null)] | ||
[InlineData("Infinity", null)] | ||
[InlineData("3e300", null)] | ||
[InlineData(null, null)] | ||
[InlineData("12.345678900", "The field RangeFloat64 must be between 12.345678901 and ∞.")] | ||
// [InlineData("-Infinity", "The field RangeFloat64 must be between 12.345678901 and ∞.")] | ||
public void Feature_Validation_ClienSideRules_RangeFloat64(string value, string error) | ||
{ | ||
RunInAllBrowsers(browser => { | ||
browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_Validation_ClientSideRules); | ||
|
||
SetValue(browser, "RangeFloat64", value); | ||
if (error == null) | ||
ExpectNoErrors(browser); | ||
else | ||
ExpectErrors(browser, new[] { error }); | ||
}); | ||
} | ||
|
||
[Theory] | ||
[InlineData("2015-01-01", null)] | ||
[InlineData("2015-12-31", null)] | ||
[InlineData(null, null)] | ||
[InlineData("", null)] | ||
[InlineData("2024-01-01", "The field RangeDate must be between 1/1/2015 and 12/31/2015.")] | ||
public void Feature_Validation_ClienSideRules_RangeDate(string value, string error) | ||
{ | ||
RunInAllBrowsers(browser => { | ||
browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_Validation_ClientSideRules); | ||
|
||
SetValue(browser, "RangeDate", value); | ||
if (error == null) | ||
ExpectNoErrors(browser); | ||
else | ||
ExpectErrors(browser, new[] { error }); | ||
}); | ||
} | ||
|
||
[Theory] | ||
[InlineData("12", null)] | ||
[InlineData(".", null)] | ||
[InlineData("", "The RequiredString field is required.")] | ||
[InlineData(" ", "The RequiredString field is required.")] | ||
[InlineData(null, "The RequiredString field is required.")] | ||
public void Feature_Validation_ClienSideRules_RequiredString(string value, string error) | ||
{ | ||
RunInAllBrowsers(browser => { | ||
browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_Validation_ClientSideRules); | ||
|
||
SetValue(browser, "RequiredString", value); | ||
if (error == null) | ||
ExpectNoErrors(browser); | ||
else | ||
ExpectErrors(browser, new[] { error }); | ||
}); | ||
} | ||
|
||
[Theory] | ||
[InlineData("12", null)] | ||
[InlineData(".", null)] | ||
[InlineData("", null)] | ||
[InlineData(" ", null)] | ||
[InlineData(null, "The NotNullString field is required.")] | ||
public void Feature_Validation_ClienSideRules_NotNullString(string value, string error) | ||
{ | ||
RunInAllBrowsers(browser => { | ||
browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_Validation_ClientSideRules); | ||
|
||
SetValue(browser, "NotNullString", value); | ||
if (error == null) | ||
ExpectNoErrors(browser); | ||
else | ||
ExpectErrors(browser, new[] { error }); | ||
}); | ||
} | ||
|
||
[Theory] | ||
[InlineData("[email protected]", null)] | ||
[InlineData(null, null)] | ||
[InlineData("@handle", "The EmailString field is not a valid e-mail address.")] | ||
[InlineData("incomplete@", "The EmailString field is not a valid e-mail address.")] | ||
[InlineData("", "The EmailString field is not a valid e-mail address.")] | ||
public void Feature_Validation_ClienSideRules_EmailString(string value, string error) | ||
{ | ||
RunInAllBrowsers(browser => { | ||
browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_Validation_ClientSideRules); | ||
|
||
SetValue(browser, "EmailString", value); | ||
if (error == null) | ||
ExpectNoErrors(browser); | ||
else | ||
ExpectErrors(browser, new[] { error }); | ||
}); | ||
} | ||
|
||
} | ||
} |