Skip to content

Commit

Permalink
validation client-side: Fix Required(AllowEmptyString=true)
Browse files Browse the repository at this point in the history
  • Loading branch information
exyi committed Feb 21, 2024
1 parent 1568d50 commit 8b5a24a
Show file tree
Hide file tree
Showing 5 changed files with 335 additions and 2 deletions.
Expand Up @@ -25,8 +25,8 @@ public virtual IEnumerable<ViewModelPropertyValidationRule> TranslateValidationR

switch (attribute)
{
case RequiredAttribute _:
validationRule.ClientRuleName = "required";
case RequiredAttribute required:
validationRule.ClientRuleName = required.AllowEmptyStrings ? "notnull" : "required";
break;
case RegularExpressionAttribute regularExpressionAttr:
validationRule.ClientRuleName = "regularExpression";
Expand Down
@@ -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; } = "test@something.somewhere";

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";
}

}
}
@@ -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.

203 changes: 203 additions & 0 deletions src/Samples/Tests/Tests/Feature/ValidationClientSideRulesTests.cs
@@ -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("a@b.c", 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 });
});
}

}
}

0 comments on commit 8b5a24a

Please sign in to comment.