/// Serializes the redirect action.
///
- public static string GenerateRedirectActionResponse(string url, bool replace, bool allowSpa)
+ public static string GenerateRedirectActionResponse(string url, bool replace, bool allowSpa, string? downloadName)
{
// create result object
var result = new JObject();
@@ -266,6 +266,7 @@ public static string GenerateRedirectActionResponse(string url, bool replace, bo
result["action"] = "redirect";
if (replace) result["replace"] = true;
if (allowSpa) result["allowSpa"] = true;
+ if (downloadName is object) result["download"] = downloadName;
return result.ToString(Formatting.None);
}
diff --git a/src/Samples/Common/ViewModels/FeatureSamples/Redirect/RedirectPostbackConcurrencyViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/Redirect/RedirectPostbackConcurrencyViewModel.cs
new file mode 100644
index 0000000000..75f53ba0b1
--- /dev/null
+++ b/src/Samples/Common/ViewModels/FeatureSamples/Redirect/RedirectPostbackConcurrencyViewModel.cs
@@ -0,0 +1,65 @@
+using System.Diagnostics.Metrics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using DotVVM.Core.Storage;
+using DotVVM.Framework.ViewModel;
+
+namespace DotVVM.Samples.BasicSamples.ViewModels.FeatureSamples.Redirect
+{
+ class RedirectPostbackConcurrencyViewModel : DotvvmViewModelBase
+ {
+ public static int GlobalCounter = 0;
+ private readonly IReturnedFileStorage returnedFileStorage;
+
+ [Bind(Direction.ServerToClient)]
+ public int Counter { get; set; } = GlobalCounter;
+
+ public int MiniCounter { get; set; } = 0;
+
+ [FromQuery("empty")]
+ public bool IsEmptyPage { get; set; } = false;
+ [FromQuery("loadDelay")]
+ public int LoadDelay { get; set; } = 0;
+
+ public RedirectPostbackConcurrencyViewModel(IReturnedFileStorage returnedFileStorage)
+ {
+ this.returnedFileStorage = returnedFileStorage;
+ }
+ public override async Task Init()
+ {
+ await Task.Delay(LoadDelay); // delay to enable user to click DelayIncrement button between it succeeding and loading the next page
+ await base.Init();
+ }
+
+ public async Task DelayIncrement()
+ {
+ await Task.Delay(1000);
+
+ Interlocked.Increment(ref GlobalCounter);
+
+ Context.RedirectToRoute(Context.Route.RouteName, query: new { empty = true, loadDelay = 2000 });
+ }
+
+ public async Task GetFileStandard()
+ {
+ await Context.ReturnFileAsync("test file"u8.ToArray(), "test.txt", "text/plain");
+ }
+
+ public async Task GetFileCustom()
+ {
+ var metadata = new ReturnedFileMetadata()
+ {
+ FileName = "test_custom.txt",
+ MimeType = "text/plain",
+ AttachmentDispositionType = "attachment"
+ };
+
+ var stream = new MemoryStream("test custom file"u8.ToArray());
+ var generatedFileId = await returnedFileStorage.StoreFileAsync(stream, metadata).ConfigureAwait(false);
+
+ var url = Context.TranslateVirtualPath("~/dotvvmReturnedFile?id=" + generatedFileId);
+ Context.RedirectToUrl(url);
+ }
+ }
+}
diff --git a/src/Samples/Common/Views/FeatureSamples/JavascriptTranslation/DateOnlyTranslations.dothtml b/src/Samples/Common/Views/FeatureSamples/JavascriptTranslation/DateOnlyTranslations.dothtml
new file mode 100644
index 0000000000..e35bb76f45
--- /dev/null
+++ b/src/Samples/Common/Views/FeatureSamples/JavascriptTranslation/DateOnlyTranslations.dothtml
@@ -0,0 +1,36 @@
+@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.JavascriptTranslation.DateTimeTranslationsViewModel, DotVVM.Samples.Common
+@import System
+
+
+
+
+
+
+
+
+
+
+
+ DateOnly testing
+
+
+
+
+
+ DateOnly.ToString:
+
+
+
+ DateOnly properties:
+
+ {{value: DateOnly.FromDateTime(NullableDateTimeProp).Day}}. {{value: DateOnly.FromDateTime(NullableDateTimeProp).Month}}. {{value: DateOnly.FromDateTime(NullableDateTimeProp).Year}}
+
+
+
+
+
+
diff --git a/src/Samples/Common/Views/FeatureSamples/JavascriptTranslation/DateTimeTranslations.dothtml b/src/Samples/Common/Views/FeatureSamples/JavascriptTranslation/DateTimeTranslations.dothtml
index d5500c9643..626044f233 100644
--- a/src/Samples/Common/Views/FeatureSamples/JavascriptTranslation/DateTimeTranslations.dothtml
+++ b/src/Samples/Common/Views/FeatureSamples/JavascriptTranslation/DateTimeTranslations.dothtml
@@ -24,11 +24,19 @@
TimeShift.setTimezoneOffset(-120);
}())
+
DateTime testing
+
+
+
Year:
diff --git a/src/Samples/Common/Views/FeatureSamples/JavascriptTranslation/TimeOnlyTranslations.dothtml b/src/Samples/Common/Views/FeatureSamples/JavascriptTranslation/TimeOnlyTranslations.dothtml
new file mode 100644
index 0000000000..3741b97752
--- /dev/null
+++ b/src/Samples/Common/Views/FeatureSamples/JavascriptTranslation/TimeOnlyTranslations.dothtml
@@ -0,0 +1,35 @@
+@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.JavascriptTranslation.DateTimeTranslationsViewModel, DotVVM.Samples.Common
+@import System
+
+
+
+
+
+
+
+
+
+
+
+ TimeOnly testing
+
+
+
+
+ TimeOnly.ToString:
+
+
+
+ TimeOnly properties:
+
+ {{value: TimeOnly.FromDateTime(NullableDateTimeProp).Hour}} hours {{value: TimeOnly.FromDateTime(NullableDateTimeProp).Minute}} minues {{value: TimeOnly.FromDateTime(NullableDateTimeProp).Second}} seconds and {{value: TimeOnly.FromDateTime(NullableDateTimeProp).Millisecond}} milliseconds
+
+
+
+
+
+
diff --git a/src/Samples/Common/Views/FeatureSamples/Localization/LocalizableRoute.dothtml b/src/Samples/Common/Views/FeatureSamples/Localization/LocalizableRoute.dothtml
index 23394a4f4c..cce5c3578b 100644
--- a/src/Samples/Common/Views/FeatureSamples/Localization/LocalizableRoute.dothtml
+++ b/src/Samples/Common/Views/FeatureSamples/Localization/LocalizableRoute.dothtml
@@ -6,6 +6,8 @@
+
+
diff --git a/src/Samples/Common/Views/FeatureSamples/Redirect/RedirectPostbackConcurrency.dothtml b/src/Samples/Common/Views/FeatureSamples/Redirect/RedirectPostbackConcurrency.dothtml
new file mode 100644
index 0000000000..f9838fcdc6
--- /dev/null
+++ b/src/Samples/Common/Views/FeatureSamples/Redirect/RedirectPostbackConcurrency.dothtml
@@ -0,0 +1,33 @@
+@viewModel DotVVM.Samples.BasicSamples.ViewModels.FeatureSamples.Redirect.RedirectPostbackConcurrencyViewModel, DotVVM.Samples.Common
+
+
+ Hello from DotVVM!
+
+
+
+ Back
+
+
+
Redirect and postback concurrency test
+
+ Testing Concurrency=Deny / Concurrency=Queue with redirect and file returns.
+
+
First, we have a set of buttons incrementing a static variable, each takes about 2sec and redirects to a blank page afterwards
+
GlobalCounter =
+
MiniCounter(Concurrency=Deny) =
+
+
+ Increment (Concurrency=Default)
+ Increment (Concurrency=Deny)
+ Increment (Concurrency=Queue)
+
+
+
We also test that returning files does not block the page forever,
+
+
+ Standard return file
+ Custom return file (will have delay before page works)
+
+
+
+
diff --git a/src/Samples/Owin/Web.config b/src/Samples/Owin/Web.config
index 22947dc087..a65c7430ad 100644
--- a/src/Samples/Owin/Web.config
+++ b/src/Samples/Owin/Web.config
@@ -66,4 +66,4 @@
-
\ No newline at end of file
+
diff --git a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs
index 22217bbc28..7549c68a19 100644
--- a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs
+++ b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs
@@ -268,12 +268,14 @@ public partial class SamplesRouteUrls
public const string FeatureSamples_IdGeneration_IdGeneration = "FeatureSamples/IdGeneration/IdGeneration";
public const string FeatureSamples_JavascriptEvents_JavascriptEvents = "FeatureSamples/JavascriptEvents/JavascriptEvents";
public const string FeatureSamples_JavascriptTranslation_ArrayTranslation = "FeatureSamples/JavascriptTranslation/ArrayTranslation";
+ public const string FeatureSamples_JavascriptTranslation_DateOnlyTranslations = "FeatureSamples/JavascriptTranslation/DateOnlyTranslations";
public const string FeatureSamples_JavascriptTranslation_DateTimeTranslations = "FeatureSamples/JavascriptTranslation/DateTimeTranslations";
public const string FeatureSamples_JavascriptTranslation_DictionaryIndexerTranslation = "FeatureSamples/JavascriptTranslation/DictionaryIndexerTranslation";
public const string FeatureSamples_JavascriptTranslation_GenericMethodTranslation = "FeatureSamples/JavascriptTranslation/GenericMethodTranslation";
public const string FeatureSamples_JavascriptTranslation_ListIndexerTranslation = "FeatureSamples/JavascriptTranslation/ListIndexerTranslation";
public const string FeatureSamples_JavascriptTranslation_ListMethodTranslations = "FeatureSamples/JavascriptTranslation/ListMethodTranslations";
public const string FeatureSamples_JavascriptTranslation_MathMethodTranslation = "FeatureSamples/JavascriptTranslation/MathMethodTranslation";
+ public const string FeatureSamples_JavascriptTranslation_TimeOnlyTranslations = "FeatureSamples/JavascriptTranslation/TimeOnlyTranslations";
public const string FeatureSamples_JavascriptTranslation_StringMethodTranslations = "FeatureSamples/JavascriptTranslation/StringMethodTranslations";
public const string FeatureSamples_JavascriptTranslation_WebUtilityTranslations = "FeatureSamples/JavascriptTranslation/WebUtilityTranslations";
public const string FeatureSamples_JsComponentIntegration_ReactComponentIntegration = "FeatureSamples/JsComponentIntegration/ReactComponentIntegration";
@@ -329,6 +331,7 @@ public partial class SamplesRouteUrls
public const string FeatureSamples_Redirect_RedirectionHelpers_PageC = "FeatureSamples/Redirect/RedirectionHelpers_PageC";
public const string FeatureSamples_Redirect_RedirectionHelpers_PageD = "FeatureSamples/Redirect/RedirectionHelpers_PageD";
public const string FeatureSamples_Redirect_RedirectionHelpers_PageE = "FeatureSamples/Redirect/RedirectionHelpers_PageE";
+ public const string FeatureSamples_Redirect_RedirectPostbackConcurrency = "FeatureSamples/Redirect/RedirectPostbackConcurrency";
public const string FeatureSamples_Redirect_Redirect_StaticCommand = "FeatureSamples/Redirect/Redirect_StaticCommand";
public const string FeatureSamples_RenderSettingsModeServer_RenderSettingModeServerProperty = "FeatureSamples/RenderSettingsModeServer/RenderSettingModeServerProperty";
public const string FeatureSamples_RenderSettingsModeServer_RepeaterCollectionExchange = "FeatureSamples/RenderSettingsModeServer/RepeaterCollectionExchange";
diff --git a/src/Samples/Tests/Tests/Complex/TaskListTests.cs b/src/Samples/Tests/Tests/Complex/TaskListTests.cs
index 09fd44c01d..2e1776e050 100644
--- a/src/Samples/Tests/Tests/Complex/TaskListTests.cs
+++ b/src/Samples/Tests/Tests/Complex/TaskListTests.cs
@@ -62,5 +62,46 @@ public void Complex_TaskList_ServerRenderedTaskList()
"Last task is not marked as completed.");
});
}
+
+ [Fact]
+ public void Complex_TaskList_TaskListAsyncCommands_ViewModelRestore()
+ {
+ // view model should be restored after back/forward navigation, but not on refresh
+ RunInAllBrowsers(browser =>
+ {
+ browser.NavigateToUrl("/");
+ browser.NavigateToUrl(SamplesRouteUrls.ComplexSamples_TaskList_TaskListAsyncCommands);
+
+ browser.SendKeys("input[type=text]", "test1");
+ browser.Click("input[type=button]");
+
+ browser.FindElements(".table tr").ThrowIfDifferentCountThan(4);
+
+ browser.NavigateBack();
+ browser.WaitUntilDotvvmInited();
+ browser.NavigateForward();
+
+ browser.FindElements(".table tr").ThrowIfDifferentCountThan(4);
+
+ browser.Refresh();
+
+ browser.FindElements(".table tr").ThrowIfDifferentCountThan(3);
+
+ browser.SendKeys("input[type=text]", "test2");
+ browser.Click("input[type=button]");
+ browser.FindElements(".table tr").ThrowIfDifferentCountThan(4);
+
+ browser.NavigateToUrl("/");
+ browser.NavigateToUrl(SamplesRouteUrls.ComplexSamples_TaskList_TaskListAsyncCommands);
+
+ browser.FindElements(".table tr").ThrowIfDifferentCountThan(3);
+
+ browser.NavigateBack();
+ browser.WaitUntilDotvvmInited();
+ browser.NavigateBack();
+
+ browser.FindElements(".table tr").ThrowIfDifferentCountThan(4);
+ });
+ }
}
}
diff --git a/src/Samples/Tests/Tests/Feature/DateTimeTranslationTests.cs b/src/Samples/Tests/Tests/Feature/DateTimeTranslationTests.cs
index 70ca58aa4a..ae6b2a3dcb 100644
--- a/src/Samples/Tests/Tests/Feature/DateTimeTranslationTests.cs
+++ b/src/Samples/Tests/Tests/Feature/DateTimeTranslationTests.cs
@@ -68,6 +68,43 @@ public void Feature_DateTime_PropertyTranslations()
});
}
+ [Fact]
+ public void Feature_DateOnly_PropertyTranslations()
+ {
+ RunInAllBrowsers(browser => {
+ browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_JavascriptTranslation_DateOnlyTranslations);
+
+ var stringDateTime = "6/28/2021 3:28:31 PM";
+
+ var textbox = browser.Single("input[data-ui=textbox]");
+ textbox.Clear().SendKeys(stringDateTime).SendEnterKey();
+
+ var str = browser.Single("span[data-ui=dateOnlyToString]");
+ AssertUI.TextEquals(str, "Monday, June 28, 2021");
+ var props = browser.Single("span[data-ui=dateOnlyProperties]");
+ AssertUI.TextEquals(props, "28. 6. 2021");
+ });
+ }
+
+ [Fact]
+ public void Feature_TimeOnly_PropertyTranslations()
+ {
+ RunInAllBrowsers(browser => {
+ browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_JavascriptTranslation_TimeOnlyTranslations);
+
+ var stringDateTime = "6/28/2021 3:28:31 PM";
+
+ var textbox = browser.Single("input[data-ui=textbox]");
+ textbox.Clear().SendKeys(stringDateTime).SendEnterKey();
+
+ var str = browser.Single("span[data-ui=timeOnlyToString]");
+ AssertUI.TextEquals(str, "3:28:31 PM");
+ var props = browser.Single("span[data-ui=timeOnlyProperties]");
+ AssertUI.TextEquals(props, "15 hours 28 minues 31 seconds and 0 milliseconds");
+ });
+ }
+
+
public DateTimeTranslationTests(ITestOutputHelper output) : base(output)
{
}
diff --git a/src/Samples/Tests/Tests/Feature/FormControlsEnabledTests.cs b/src/Samples/Tests/Tests/Feature/FormControlsEnabledTests.cs
index 79c468a479..7a47b04a0c 100644
--- a/src/Samples/Tests/Tests/Feature/FormControlsEnabledTests.cs
+++ b/src/Samples/Tests/Tests/Feature/FormControlsEnabledTests.cs
@@ -84,7 +84,7 @@ public void Feature_FormControlsEnabled_FormControlsEnabled()
private void TestLinkButton(IBrowserWrapper browser, string id, bool shouldBeEnabled, ref int currentPresses)
{
- browser.First($"#{id}").Click();
+ browser.First($"#{id}").ScrollTo().Wait(500).Click();
if (shouldBeEnabled)
{
currentPresses++;
diff --git a/src/Samples/Tests/Tests/Feature/LocalizationTests.cs b/src/Samples/Tests/Tests/Feature/LocalizationTests.cs
index e3b56c741b..5d26fb5071 100644
--- a/src/Samples/Tests/Tests/Feature/LocalizationTests.cs
+++ b/src/Samples/Tests/Tests/Feature/LocalizationTests.cs
@@ -146,6 +146,8 @@ public void Feature_Localization_LocalizableRoute()
AssertUI.Attribute(links[1], "href", v => v.EndsWith("/de/FeatureSamples/Localization/lokalisierte-route"));
AssertUI.Attribute(links[2], "href", v => v.EndsWith("/FeatureSamples/Localization/LocalizableRoute"));
AssertUI.Attribute(links[3], "href", links[2].GetAttribute("href"));
+ AssertAlternateLink("cs-cz", "/cs/FeatureSamples/Localization/lokalizovana-routa");
+ AssertAlternateLink("de", "/de/FeatureSamples/Localization/lokalisierte-route");
links[0].Click().Wait(500);
culture = browser.Single("span[data-ui=culture]");
@@ -155,6 +157,8 @@ public void Feature_Localization_LocalizableRoute()
AssertUI.Attribute(links[1], "href", v => v.EndsWith("/de/FeatureSamples/Localization/lokalisierte-route"));
AssertUI.Attribute(links[2], "href", v => v.EndsWith("/FeatureSamples/Localization/LocalizableRoute"));
AssertUI.Attribute(links[3], "href", links[0].GetAttribute("href"));
+ AssertAlternateLink("x-default", "/FeatureSamples/Localization/LocalizableRoute");
+ AssertAlternateLink("de", "/de/FeatureSamples/Localization/lokalisierte-route");
links[1].Click().Wait(500);
culture = browser.Single("span[data-ui=culture]");
@@ -164,6 +168,8 @@ public void Feature_Localization_LocalizableRoute()
AssertUI.Attribute(links[1], "href", v => v.EndsWith("/de/FeatureSamples/Localization/lokalisierte-route"));
AssertUI.Attribute(links[2], "href", v => v.EndsWith("/FeatureSamples/Localization/LocalizableRoute"));
AssertUI.Attribute(links[3], "href", links[1].GetAttribute("href"));
+ AssertAlternateLink("x-default", "/FeatureSamples/Localization/LocalizableRoute");
+ AssertAlternateLink("cs-cz", "/cs/FeatureSamples/Localization/lokalizovana-routa");
links[2].Click().Wait(500);
culture = browser.Single("span[data-ui=culture]");
@@ -173,6 +179,13 @@ public void Feature_Localization_LocalizableRoute()
AssertUI.Attribute(links[1], "href", v => v.EndsWith("/de/FeatureSamples/Localization/lokalisierte-route"));
AssertUI.Attribute(links[2], "href", v => v.EndsWith("/FeatureSamples/Localization/LocalizableRoute"));
AssertUI.Attribute(links[3], "href", links[2].GetAttribute("href"));
+ AssertAlternateLink("cs-cz", "/cs/FeatureSamples/Localization/lokalizovana-routa");
+ AssertAlternateLink("de", "/de/FeatureSamples/Localization/lokalisierte-route");
+
+ void AssertAlternateLink(string culture, string url)
+ {
+ AssertUI.Attribute(browser.Single($"link[rel=alternate][hreflang={culture}]"), "href", this.TestSuiteRunner.Configuration.BaseUrls[0].TrimEnd('/') + url);
+ }
});
}
diff --git a/src/Samples/Tests/Tests/Feature/RedirectTests.cs b/src/Samples/Tests/Tests/Feature/RedirectTests.cs
index 70af6f5cc2..ef9b12b62e 100644
--- a/src/Samples/Tests/Tests/Feature/RedirectTests.cs
+++ b/src/Samples/Tests/Tests/Feature/RedirectTests.cs
@@ -1,7 +1,11 @@
using System;
using System.Linq;
+using System.Threading;
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;
@@ -69,6 +73,94 @@ public void Feature_Redirect_RedirectionHelpers()
Assert.Matches($"{SamplesRouteUrls.FeatureSamples_Redirect_RedirectionHelpers_PageE}/1221\\?x=a", currentUrl.LocalPath + currentUrl.Query);
});
}
-
+
+ bool TryClick(IElementWrapper element)
+ {
+ if (element is null) return false;
+ try
+ {
+ element.Click();
+ return true;
+ }
+ catch (StaleElementReferenceException)
+ {
+ return false;
+ }
+ }
+
+ [Fact]
+ public void Feature_Redirect_RedirectPostbackConcurrency()
+ {
+ RunInAllBrowsers(browser => {
+ browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_Redirect_RedirectPostbackConcurrency);
+
+ int globalCounter() => int.Parse(browser.First("counter", SelectByDataUi).GetText());
+
+ var initialCounter = globalCounter();
+ for (int i = 0; i < 20; i++)
+ {
+ TryClick(browser.FirstOrDefault("inc-default", SelectByDataUi));
+ Thread.Sleep(1);
+ }
+ browser.WaitFor(() => Assert.Contains("empty=true", browser.CurrentUrl, StringComparison.OrdinalIgnoreCase), 7000, "Redirect did not happen");
+ browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_Redirect_RedirectPostbackConcurrency);
+
+ // must increment at least 20 times, otherwise delays are too short
+ Assert.Equal(globalCounter(), initialCounter + 20);
+
+ initialCounter = globalCounter();
+ var clickCount = 0;
+ while (TryClick(browser.FirstOrDefault("inc-deny", SelectByDataUi)))
+ {
+ clickCount++;
+ Thread.Sleep(1);
+ }
+ Assert.InRange(clickCount, 3, int.MaxValue);
+ browser.WaitFor(() => Assert.Contains("empty=true", browser.CurrentUrl, StringComparison.OrdinalIgnoreCase), timeout: 500, "Redirect did not happen");
+
+ browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_Redirect_RedirectPostbackConcurrency);
+ Assert.Equal(globalCounter(), initialCounter + 1); // only one click was allowed
+
+ initialCounter = globalCounter();
+ clickCount = 0;
+ while (TryClick(browser.FirstOrDefault("inc-queue", SelectByDataUi)))
+ {
+ clickCount++;
+ Thread.Sleep(1);
+ }
+
+ Assert.InRange(clickCount, 3, int.MaxValue);
+ browser.WaitFor(() => Assert.Contains("empty=true", browser.CurrentUrl, StringComparison.OrdinalIgnoreCase), timeout: 500, "Redirect did not happen");
+
+ browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_Redirect_RedirectPostbackConcurrency);
+ Assert.Equal(globalCounter(), initialCounter + 1); // only one click was allowed
+ });
+ }
+
+ [Fact]
+ public void Feature_Redirect_RedirectPostbackConcurrencyFileReturn()
+ {
+ RunInAllBrowsers(browser => {
+ browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_Redirect_RedirectPostbackConcurrency);
+
+ void increment(int timeout)
+ {
+ browser.WaitFor(() => {
+ var original = int.Parse(browser.First("minicounter", SelectByDataUi).GetText());
+ browser.First("minicounter", SelectByDataUi).Click();
+ AssertUI.TextEquals(browser.First("minicounter", SelectByDataUi), (original + 1).ToString());
+ }, timeout, "Could not increment minicounter in given timeout (postback queue is blocked)");
+ }
+
+ increment(3000);
+
+ browser.First("file-std", SelectByDataUi).Click();
+ increment(3000);
+
+ browser.First("file-custom", SelectByDataUi).Click();
+ // longer timeout, because DotVVM blocks postback queue for 5s after redirects to debounce any further requests
+ increment(15000);
+ });
+ }
}
}
diff --git a/src/Tests/Binding/JavascriptCompilationTests.cs b/src/Tests/Binding/JavascriptCompilationTests.cs
index d387558513..7b9aa99c94 100644
--- a/src/Tests/Binding/JavascriptCompilationTests.cs
+++ b/src/Tests/Binding/JavascriptCompilationTests.cs
@@ -1112,6 +1112,17 @@ public void JsTranslator_DateTime_Property_Getters(string binding, string jsFunc
Assert.AreEqual($"dotvvm.serialization.parseDate(DateTime()).{jsFunction}(){(increment ? "+1" : string.Empty)}", result);
}
+ [TestMethod]
+ [DataRow("DateTime.Now", "dotvvm.serialization.serializeDate(new Date(),false)")]
+ [DataRow("DateTime.UtcNow", "dotvvm.serialization.serializeDate(new Date(),true)")]
+ [DataRow("DateTime.Today", "dotvvm.serialization.serializeDate(new Date(),false).substring(0,10)+\"T00:00:00.000\"")]
+ public void JsTranslator_DateTime_Now(string binding, string expected)
+ {
+ var result = CompileBinding(binding);
+ Assert.AreEqual(expected, result);
+ }
+
+
[TestMethod]
[DataRow("DateOnly.ToString()", "")]
[DataRow("DateOnly.ToString('D')", "\"D\"")]
@@ -1129,6 +1140,16 @@ public void JsTranslator_NullableDateOnly_ToString(string binding, string args)
Assert.AreEqual($"dotvvm.globalize.bindingDateOnlyToString(NullableDateOnly{((args.Length > 0) ? $",{args}" : string.Empty)})", result);
}
+ [DataTestMethod]
+ [DataRow("DateOnly.Year", "getFullYear()")]
+ [DataRow("DateOnly.Month", "getMonth()+1")]
+ [DataRow("DateOnly.Day", "getDate()")]
+ public void JsTranslator_DateOnly_Property_Getters(string binding, string jsFunction)
+ {
+ var result = CompileBinding(binding, new[] { typeof(TestViewModel) });
+ Assert.AreEqual($"dotvvm.serialization.parseDateOnly(DateOnly()).{jsFunction}", result);
+ }
+
[TestMethod]
[DataRow("TimeOnly.ToString()", "")]
[DataRow("TimeOnly.ToString('T')", "\"T\"")]
@@ -1146,6 +1167,17 @@ public void JsTranslator_NullableTimeOnly_ToString(string binding, string args)
Assert.AreEqual($"dotvvm.globalize.bindingTimeOnlyToString(NullableTimeOnly{((args.Length > 0) ? $",{args}" : string.Empty)})", result);
}
+ [DataTestMethod]
+ [DataRow("TimeOnly.Hour", "getHours()")]
+ [DataRow("TimeOnly.Minute", "getMinutes()")]
+ [DataRow("TimeOnly.Second", "getSeconds()")]
+ [DataRow("TimeOnly.Millisecond", "getMilliseconds()")]
+ public void JsTranslator_TimeOnly_Property_Getters(string binding, string jsFunction)
+ {
+ var result = CompileBinding(binding, new[] { typeof(TestViewModel) });
+ Assert.AreEqual($"dotvvm.serialization.parseTimeOnly(TimeOnly()).{jsFunction}", result);
+ }
+
[TestMethod]
public void JsTranslator_WebUtility_UrlEncode()
{
diff --git a/src/Tests/Parser/Dothtml/DothtmlTokenizerElementsTests.cs b/src/Tests/Parser/Dothtml/DothtmlTokenizerElementsTests.cs
index 0fd1c4fc11..697f0b2455 100644
--- a/src/Tests/Parser/Dothtml/DothtmlTokenizerElementsTests.cs
+++ b/src/Tests/Parser/Dothtml/DothtmlTokenizerElementsTests.cs
@@ -239,6 +239,18 @@ public void DothtmlTokenizer_Invalid_OpenBraceInText()
tokenizer.Tokenize(input);
Assert.IsTrue(tokenizer.Tokens.All(t => t.Type == DothtmlTokenType.Text));
+ Assert.IsTrue(tokenizer.Tokens.All(t => !t.HasError));
+ Assert.AreEqual(string.Concat(tokenizer.Tokens.Select(t => t.Text)), input);
+ }
+
+ [TestMethod]
+ public void DothtmlTokenizer_Invalid_OpenBraceInTextWithoutSpace()
+ {
+ var input = "inline