FormsTest.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. // Copyright (c) .NET Foundation. All rights reserved.
  2. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  3. using BasicTestApp;
  4. using BasicTestApp.FormsTest;
  5. using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
  6. using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
  7. using Microsoft.AspNetCore.E2ETesting;
  8. using Microsoft.AspNetCore.Testing;
  9. using Microsoft.AspNetCore.Testing.xunit;
  10. using OpenQA.Selenium;
  11. using OpenQA.Selenium.Support.UI;
  12. using System;
  13. using System.Linq;
  14. using System.Threading.Tasks;
  15. using Xunit;
  16. using Xunit.Abstractions;
  17. namespace Microsoft.AspNetCore.Components.E2ETest.Tests
  18. {
  19. public class FormsTest : BasicTestAppTestBase
  20. {
  21. public FormsTest(
  22. BrowserFixture browserFixture,
  23. ToggleExecutionModeServerFixture<Program> serverFixture,
  24. ITestOutputHelper output)
  25. : base(browserFixture, serverFixture, output)
  26. {
  27. }
  28. protected override void InitializeAsyncCore()
  29. {
  30. // On WebAssembly, page reloads are expensive so skip if possible
  31. Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost);
  32. }
  33. [Fact]
  34. public async Task EditFormWorksWithDataAnnotationsValidator()
  35. {
  36. var appElement = MountTestComponent<SimpleValidationComponent>();
  37. var userNameInput = appElement.FindElement(By.ClassName("user-name")).FindElement(By.TagName("input"));
  38. var acceptsTermsInput = appElement.FindElement(By.ClassName("accepts-terms")).FindElement(By.TagName("input"));
  39. var submitButton = appElement.FindElement(By.TagName("button"));
  40. var messagesAccessor = CreateValidationMessagesAccessor(appElement);
  41. // Editing a field doesn't trigger validation on its own
  42. userNameInput.SendKeys("Bert\t");
  43. acceptsTermsInput.Click(); // Accept terms
  44. acceptsTermsInput.Click(); // Un-accept terms
  45. await Task.Delay(500); // There's no expected change to the UI, so just wait a moment before asserting
  46. Browser.Empty(messagesAccessor);
  47. Assert.Empty(appElement.FindElements(By.Id("last-callback")));
  48. // Submitting the form does validate
  49. submitButton.Click();
  50. Browser.Equal(new[] { "You must accept the terms" }, messagesAccessor);
  51. Browser.Equal("OnInvalidSubmit", () => appElement.FindElement(By.Id("last-callback")).Text);
  52. // Can make another field invalid
  53. userNameInput.Clear();
  54. submitButton.Click();
  55. Browser.Equal(new[] { "Please choose a username", "You must accept the terms" }, messagesAccessor);
  56. Browser.Equal("OnInvalidSubmit", () => appElement.FindElement(By.Id("last-callback")).Text);
  57. // Can make valid
  58. userNameInput.SendKeys("Bert\t");
  59. acceptsTermsInput.Click();
  60. submitButton.Click();
  61. Browser.Empty(messagesAccessor);
  62. Browser.Equal("OnValidSubmit", () => appElement.FindElement(By.Id("last-callback")).Text);
  63. }
  64. [Fact]
  65. public void InputTextInteractsWithEditContext()
  66. {
  67. var appElement = MountTestComponent<TypicalValidationComponent>();
  68. var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input"));
  69. var messagesAccessor = CreateValidationMessagesAccessor(appElement);
  70. // Validates on edit
  71. Browser.Equal("valid", () => nameInput.GetAttribute("class"));
  72. nameInput.SendKeys("Bert\t");
  73. Browser.Equal("modified valid", () => nameInput.GetAttribute("class"));
  74. // Can become invalid
  75. nameInput.SendKeys("01234567890123456789\t");
  76. Browser.Equal("modified invalid", () => nameInput.GetAttribute("class"));
  77. Browser.Equal(new[] { "That name is too long" }, messagesAccessor);
  78. // Can become valid
  79. nameInput.Clear();
  80. nameInput.SendKeys("Bert\t");
  81. Browser.Equal("modified valid", () => nameInput.GetAttribute("class"));
  82. Browser.Empty(messagesAccessor);
  83. }
  84. [Fact]
  85. public void InputNumberInteractsWithEditContext_NonNullableInt()
  86. {
  87. var appElement = MountTestComponent<TypicalValidationComponent>();
  88. var ageInput = appElement.FindElement(By.ClassName("age")).FindElement(By.TagName("input"));
  89. var messagesAccessor = CreateValidationMessagesAccessor(appElement);
  90. // Validates on edit
  91. Browser.Equal("valid", () => ageInput.GetAttribute("class"));
  92. ageInput.SendKeys("123\t");
  93. Browser.Equal("modified valid", () => ageInput.GetAttribute("class"));
  94. // Can become invalid
  95. ageInput.SendKeys("e100\t");
  96. Browser.Equal("modified invalid", () => ageInput.GetAttribute("class"));
  97. Browser.Equal(new[] { "The AgeInYears field must be a number." }, messagesAccessor);
  98. // Empty is invalid, because it's not a nullable int
  99. ageInput.Clear();
  100. ageInput.SendKeys("\t");
  101. Browser.Equal("modified invalid", () => ageInput.GetAttribute("class"));
  102. Browser.Equal(new[] { "The AgeInYears field must be a number." }, messagesAccessor);
  103. // Zero is within the allowed range
  104. ageInput.SendKeys("0\t");
  105. Browser.Equal("modified valid", () => ageInput.GetAttribute("class"));
  106. Browser.Empty(messagesAccessor);
  107. }
  108. [Fact]
  109. public void InputNumberInteractsWithEditContext_NullableFloat()
  110. {
  111. var appElement = MountTestComponent<TypicalValidationComponent>();
  112. var heightInput = appElement.FindElement(By.ClassName("height")).FindElement(By.TagName("input"));
  113. var messagesAccessor = CreateValidationMessagesAccessor(appElement);
  114. // Validates on edit
  115. Browser.Equal("valid", () => heightInput.GetAttribute("class"));
  116. heightInput.SendKeys("123.456\t");
  117. Browser.Equal("modified valid", () => heightInput.GetAttribute("class"));
  118. // Can become invalid
  119. heightInput.SendKeys("e100\t");
  120. Browser.Equal("modified invalid", () => heightInput.GetAttribute("class"));
  121. Browser.Equal(new[] { "The OptionalHeight field must be a number." }, messagesAccessor);
  122. // Empty is valid, because it's a nullable float
  123. heightInput.Clear();
  124. heightInput.SendKeys("\t");
  125. Browser.Equal("modified valid", () => heightInput.GetAttribute("class"));
  126. Browser.Empty(messagesAccessor);
  127. }
  128. [Fact]
  129. public void InputTextAreaInteractsWithEditContext()
  130. {
  131. var appElement = MountTestComponent<TypicalValidationComponent>();
  132. var descriptionInput = appElement.FindElement(By.ClassName("description")).FindElement(By.TagName("textarea"));
  133. var messagesAccessor = CreateValidationMessagesAccessor(appElement);
  134. // Validates on edit
  135. Browser.Equal("valid", () => descriptionInput.GetAttribute("class"));
  136. descriptionInput.SendKeys("Hello\t");
  137. Browser.Equal("modified valid", () => descriptionInput.GetAttribute("class"));
  138. // Can become invalid
  139. descriptionInput.SendKeys("too long too long too long too long too long\t");
  140. Browser.Equal("modified invalid", () => descriptionInput.GetAttribute("class"));
  141. Browser.Equal(new[] { "Description is max 20 chars" }, messagesAccessor);
  142. // Can become valid
  143. descriptionInput.Clear();
  144. descriptionInput.SendKeys("Hello\t");
  145. Browser.Equal("modified valid", () => descriptionInput.GetAttribute("class"));
  146. Browser.Empty(messagesAccessor);
  147. }
  148. [Fact]
  149. public void InputDateInteractsWithEditContext_NonNullableDateTime()
  150. {
  151. var appElement = MountTestComponent<TypicalValidationComponent>();
  152. var renewalDateInput = appElement.FindElement(By.ClassName("renewal-date")).FindElement(By.TagName("input"));
  153. var messagesAccessor = CreateValidationMessagesAccessor(appElement);
  154. // Validates on edit
  155. Browser.Equal("valid", () => renewalDateInput.GetAttribute("class"));
  156. renewalDateInput.SendKeys("01/01/2000\t");
  157. Browser.Equal("modified valid", () => renewalDateInput.GetAttribute("class"));
  158. // Can become invalid
  159. renewalDateInput.SendKeys("0/0/0");
  160. Browser.Equal("modified invalid", () => renewalDateInput.GetAttribute("class"));
  161. Browser.Equal(new[] { "The RenewalDate field must be a date." }, messagesAccessor);
  162. // Empty is invalid, because it's not nullable
  163. renewalDateInput.SendKeys($"{Keys.Backspace}\t{Keys.Backspace}\t{Keys.Backspace}\t");
  164. Browser.Equal("modified invalid", () => renewalDateInput.GetAttribute("class"));
  165. Browser.Equal(new[] { "The RenewalDate field must be a date." }, messagesAccessor);
  166. // Can become valid
  167. renewalDateInput.SendKeys("01/01/01\t");
  168. Browser.Equal("modified valid", () => renewalDateInput.GetAttribute("class"));
  169. Browser.Empty(messagesAccessor);
  170. }
  171. [Fact]
  172. [Flaky("https://github.com/aspnet/AspNetCore-Internal/issues/2511", FlakyOn.All)]
  173. public void InputDateInteractsWithEditContext_NullableDateTimeOffset()
  174. {
  175. var appElement = MountTestComponent<TypicalValidationComponent>();
  176. var expiryDateInput = appElement.FindElement(By.ClassName("expiry-date")).FindElement(By.TagName("input"));
  177. var messagesAccessor = CreateValidationMessagesAccessor(appElement);
  178. // Validates on edit
  179. Browser.Equal("valid", () => expiryDateInput.GetAttribute("class"));
  180. expiryDateInput.SendKeys("01/01/2000\t");
  181. Browser.Equal("modified valid", () => expiryDateInput.GetAttribute("class"));
  182. // Can become invalid
  183. expiryDateInput.Clear();
  184. expiryDateInput.SendKeys("111111111");
  185. Browser.Equal("modified invalid", () => expiryDateInput.GetAttribute("class"));
  186. Browser.Equal(new[] { "The OptionalExpiryDate field must be a date." }, messagesAccessor);
  187. // Empty is valid, because it's nullable
  188. expiryDateInput.SendKeys($"{Keys.Backspace}\t{Keys.Backspace}\t{Keys.Backspace}\t");
  189. Browser.Equal("modified valid", () => expiryDateInput.GetAttribute("class"));
  190. Browser.Empty(messagesAccessor);
  191. }
  192. [Fact]
  193. public void InputSelectInteractsWithEditContext()
  194. {
  195. var appElement = MountTestComponent<TypicalValidationComponent>();
  196. var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("ticket-class")).FindElement(By.TagName("select")));
  197. var select = ticketClassInput.WrappedElement;
  198. var messagesAccessor = CreateValidationMessagesAccessor(appElement);
  199. // Validates on edit
  200. Browser.Equal("valid", () => select.GetAttribute("class"));
  201. ticketClassInput.SelectByText("First class");
  202. Browser.Equal("modified valid", () => select.GetAttribute("class"));
  203. // Can become invalid
  204. ticketClassInput.SelectByText("(select)");
  205. Browser.Equal("modified invalid", () => select.GetAttribute("class"));
  206. Browser.Equal(new[] { "The TicketClass field is not valid." }, messagesAccessor);
  207. }
  208. [Fact]
  209. public void InputCheckboxInteractsWithEditContext()
  210. {
  211. var appElement = MountTestComponent<TypicalValidationComponent>();
  212. var acceptsTermsInput = appElement.FindElement(By.ClassName("accepts-terms")).FindElement(By.TagName("input"));
  213. var isEvilInput = appElement.FindElement(By.ClassName("is-evil")).FindElement(By.TagName("input"));
  214. var messagesAccessor = CreateValidationMessagesAccessor(appElement);
  215. // Correct initial checkedness
  216. Assert.False(acceptsTermsInput.Selected);
  217. Assert.True(isEvilInput.Selected);
  218. // Validates on edit
  219. Browser.Equal("valid", () => acceptsTermsInput.GetAttribute("class"));
  220. Browser.Equal("valid", () => isEvilInput.GetAttribute("class"));
  221. acceptsTermsInput.Click();
  222. isEvilInput.Click();
  223. Browser.Equal("modified valid", () => acceptsTermsInput.GetAttribute("class"));
  224. Browser.Equal("modified valid", () => isEvilInput.GetAttribute("class"));
  225. // Can become invalid
  226. acceptsTermsInput.Click();
  227. isEvilInput.Click();
  228. Browser.Equal("modified invalid", () => acceptsTermsInput.GetAttribute("class"));
  229. Browser.Equal("modified invalid", () => isEvilInput.GetAttribute("class"));
  230. Browser.Equal(new[] { "Must accept terms", "Must not be evil" }, messagesAccessor);
  231. }
  232. [Fact]
  233. public void CanWireUpINotifyPropertyChangedToEditContext()
  234. {
  235. var appElement = MountTestComponent<NotifyPropertyChangedValidationComponent>();
  236. var userNameInput = appElement.FindElement(By.ClassName("user-name")).FindElement(By.TagName("input"));
  237. var acceptsTermsInput = appElement.FindElement(By.ClassName("accepts-terms")).FindElement(By.TagName("input"));
  238. var submitButton = appElement.FindElement(By.TagName("button"));
  239. var messagesAccessor = CreateValidationMessagesAccessor(appElement);
  240. var submissionStatus = appElement.FindElement(By.Id("submission-status"));
  241. // Editing a field triggers validation immediately
  242. Browser.Equal("valid", () => userNameInput.GetAttribute("class"));
  243. userNameInput.SendKeys("Too long too long\t");
  244. Browser.Equal("modified invalid", () => userNameInput.GetAttribute("class"));
  245. Browser.Equal(new[] { "That name is too long" }, messagesAccessor);
  246. // Submitting the form validates remaining fields
  247. submitButton.Click();
  248. Browser.Equal(new[] { "That name is too long", "You must accept the terms" }, messagesAccessor);
  249. Browser.Equal("modified invalid", () => userNameInput.GetAttribute("class"));
  250. Browser.Equal("invalid", () => acceptsTermsInput.GetAttribute("class"));
  251. // Can make fields valid
  252. userNameInput.Clear();
  253. userNameInput.SendKeys("Bert\t");
  254. Browser.Equal("modified valid", () => userNameInput.GetAttribute("class"));
  255. acceptsTermsInput.Click();
  256. Browser.Equal("modified valid", () => acceptsTermsInput.GetAttribute("class"));
  257. Browser.Equal(string.Empty, () => submissionStatus.Text);
  258. submitButton.Click();
  259. Browser.True(() => submissionStatus.Text.StartsWith("Submitted"));
  260. // Fields can revert to unmodified
  261. Browser.Equal("valid", () => userNameInput.GetAttribute("class"));
  262. Browser.Equal("valid", () => acceptsTermsInput.GetAttribute("class"));
  263. }
  264. [Fact]
  265. public void ValidationMessageDisplaysMessagesForField()
  266. {
  267. var appElement = MountTestComponent<TypicalValidationComponent>();
  268. var emailContainer = appElement.FindElement(By.ClassName("email"));
  269. var emailInput = emailContainer.FindElement(By.TagName("input"));
  270. var emailMessagesAccessor = CreateValidationMessagesAccessor(emailContainer);
  271. var submitButton = appElement.FindElement(By.TagName("button"));
  272. // Doesn't show messages for other fields
  273. submitButton.Click();
  274. Browser.Empty(emailMessagesAccessor);
  275. // Updates on edit
  276. emailInput.SendKeys("abc\t");
  277. Browser.Equal(new[] { "That doesn't look like a real email address" }, emailMessagesAccessor);
  278. // Can show more than one message
  279. emailInput.SendKeys("too long too long too long\t");
  280. Browser.Equal(new[] { "That doesn't look like a real email address", "We only accept very short email addresses (max 10 chars)" }, emailMessagesAccessor);
  281. // Can become valid
  282. emailInput.Clear();
  283. emailInput.SendKeys("[email protected]\t");
  284. Browser.Empty(emailMessagesAccessor);
  285. }
  286. [Fact]
  287. public void InputComponentsCauseContainerToRerenderOnChange()
  288. {
  289. var appElement = MountTestComponent<TypicalValidationComponent>();
  290. var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("ticket-class")).FindElement(By.TagName("select")));
  291. var selectedTicketClassDisplay = appElement.FindElement(By.Id("selected-ticket-class"));
  292. var messagesAccessor = CreateValidationMessagesAccessor(appElement);
  293. // Shows initial state
  294. Browser.Equal("Economy", () => selectedTicketClassDisplay.Text);
  295. // Refreshes on edit
  296. ticketClassInput.SelectByValue("Premium");
  297. Browser.Equal("Premium", () => selectedTicketClassDisplay.Text);
  298. // Leaves previous value unchanged if new entry is unparseable
  299. ticketClassInput.SelectByText("(select)");
  300. Browser.Equal(new[] { "The TicketClass field is not valid." }, messagesAccessor);
  301. Browser.Equal("Premium", () => selectedTicketClassDisplay.Text);
  302. }
  303. private Func<string[]> CreateValidationMessagesAccessor(IWebElement appElement)
  304. {
  305. return () => appElement.FindElements(By.ClassName("validation-message"))
  306. .Select(x => x.Text)
  307. .OrderBy(x => x)
  308. .ToArray();
  309. }
  310. }
  311. }