From 259476a63ed8c8ee9a0d61bc4755b65b568d9549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 18 Feb 2024 22:30:17 +0100 Subject: [PATCH 1/5] New dot:ModalDialog control, wrapper for The control closes or shows the modal based on an Open property. The modal is always shown using the .showModal() method, non-modal dialog is already accessible by binding the open attribute of dialog HTML element. The Open property may either be a boolean or a nullable object, the dialog is shown if the value isn't false nor null. On close event, false or null is written back into the Open property. Otherwise, we'd quickly have inconsistent viewModel whenever the user closes the dialog with ESC. Close event is also provided for explicit event handling. We also optionaly provide a helper for implementing "close after backdrop click" functionality. It is not supported by the dialog element natively and could not be otherwise implemented without writing custom JS. It is enabled by setting CloseOnBackdropClick=true Resolves #1708 --- .../Framework/Controls/ModalDialog.cs | 81 +++++++++++++ .../Scripts/binding-handlers/all-handlers.ts | 4 +- .../Scripts/binding-handlers/modal-dialog.ts | 41 +++++++ .../ModalDialog/ModalDialogViewModel.cs | 42 +++++++ .../ModalDialog/ModalDialog.dothtml | 79 ++++++++++++ .../Abstractions/SamplesRouteUrls.designer.cs | 1 + .../Tests/Tests/Feature/ModalDialogTests.cs | 112 ++++++++++++++++++ 7 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 src/Framework/Framework/Controls/ModalDialog.cs create mode 100644 src/Framework/Framework/Resources/Scripts/binding-handlers/modal-dialog.ts create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/ModalDialog/ModalDialogViewModel.cs create mode 100644 src/Samples/Common/Views/FeatureSamples/ModalDialog/ModalDialog.dothtml create mode 100644 src/Samples/Tests/Tests/Feature/ModalDialogTests.cs diff --git a/src/Framework/Framework/Controls/ModalDialog.cs b/src/Framework/Framework/Controls/ModalDialog.cs new file mode 100644 index 0000000000..a81a6e497f --- /dev/null +++ b/src/Framework/Framework/Controls/ModalDialog.cs @@ -0,0 +1,81 @@ +using System; +using System.Net; +using System.Text; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Hosting; +using DotVVM.Framework.ResourceManagement; +using Newtonsoft.Json; + +namespace DotVVM.Framework.Controls +{ + /// + /// Renders a HTML native dialog element, it is opened using the showModal function when the property is set to true + /// + /// + /// * Non-modal dialogs may be simply binding the attribute of the HTML dialog element + /// * The dialog may be closed by button with formmethod="dialog", when ESC is pressed, or when the backdrop is clicked if = true + /// + [ControlMarkupOptions()] + public class ModalDialog : HtmlGenericControl + { + public ModalDialog() + : base("dialog", false) + { + } + + /// A value indicating whether the dialog is open. The value can either be a boolean or an object (not false or not null -> shown). On close, the value is written back into the Open binding. + [MarkupOptions(AllowHardCodedValue = false)] + public object Open + { + get { return (bool?)GetValue(OpenProperty) ?? false; } + set { SetValue(OpenProperty, value); } + } + public static readonly DotvvmProperty OpenProperty = + DotvvmProperty.Register(nameof(Open), false); + + /// Add an event handler which closes the dialog when the backdrop is clicked. + public bool CloseOnBackdropClick + { + get { return (bool?)GetValue(CloseOnBackdropClickProperty) ?? false; } + set { SetValue(CloseOnBackdropClickProperty, value); } + } + public static readonly DotvvmProperty CloseOnBackdropClickProperty = + DotvvmProperty.Register(nameof(CloseOnBackdropClick), false); + + /// Triggered when the dialog is closed. Called regardless if it was closed by user input or by change. + public Command? Close + { + get { return (Command?)GetValue(CloseProperty); } + set { SetValue(CloseProperty, value); } + } + public static readonly DotvvmProperty CloseProperty = + DotvvmProperty.Register(nameof(Close)); + + protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context) + { + var valueBinding = GetValueBinding(OpenProperty); + if (valueBinding is {}) + { + writer.AddKnockoutDataBind("dotvvm-modal-open", this, valueBinding); + } + else if (!(Open is false or null)) + { + // we have to use the binding handler instead of `open` attribute, because we need to call the showModal function + writer.AddKnockoutDataBind("dotvvm-modal-open", "true"); + } + + if (GetValueOrBinding(CloseOnBackdropClickProperty) is {} x && !x.ValueEquals(false)) + { + writer.AddKnockoutDataBind("dotvvm-model-backdrop-close", x.GetJsExpression(this)); + } + + if (GetCommandBinding(CloseProperty) is {} close) + { + writer.AddAttribute("onclose", KnockoutHelper.GenerateClientPostBackScript(nameof(Close), close, this, returnValue: null)); + } + + base.AddAttributesToRender(writer, context); + } + } +} diff --git a/src/Framework/Framework/Resources/Scripts/binding-handlers/all-handlers.ts b/src/Framework/Framework/Resources/Scripts/binding-handlers/all-handlers.ts index ccacf44549..6480b3a0a2 100644 --- a/src/Framework/Framework/Resources/Scripts/binding-handlers/all-handlers.ts +++ b/src/Framework/Framework/Resources/Scripts/binding-handlers/all-handlers.ts @@ -10,6 +10,7 @@ import gridviewdataset from './gridviewdataset' import namedCommand from './named-command' import fileUpload from './file-upload' import jsComponents from './js-component' +import modalDialog from './modal-dialog' type KnockoutHandlerDictionary = { [name: string]: KnockoutBindingHandler @@ -26,7 +27,8 @@ const allHandlers: KnockoutHandlerDictionary = { ...gridviewdataset, ...namedCommand, ...fileUpload, - ...jsComponents + ...jsComponents, + ...modalDialog } export default allHandlers diff --git a/src/Framework/Framework/Resources/Scripts/binding-handlers/modal-dialog.ts b/src/Framework/Framework/Resources/Scripts/binding-handlers/modal-dialog.ts new file mode 100644 index 0000000000..b745237de9 --- /dev/null +++ b/src/Framework/Framework/Resources/Scripts/binding-handlers/modal-dialog.ts @@ -0,0 +1,41 @@ +export default { + "dotvvm-modal-open": { + init(element: HTMLDialogElement, valueAccessor: () => any) { + element.addEventListener("close", () => { + const value = valueAccessor(); + if (ko.isWriteableObservable(value)) { + // if the value is object, set it to null + value(typeof value.peek() == "boolean" ? false : null) + } + }) + }, + update(element: HTMLDialogElement, valueAccessor: () => any) { + const value = ko.unwrap(valueAccessor()), + shouldOpen = value != null && value !== false; + if (shouldOpen != element.open) { + if (shouldOpen) { + element.showModal() + } else { + element.close() + } + } + }, + }, + "dotvvm-model-backdrop-close": { + init(element: HTMLDialogElement, valueAccessor: () => any) { + // closes the dialog when the backdrop is clicked + element.addEventListener("click", (e) => { + if (e.target == element) { + const elementRect = element.getBoundingClientRect(), + x = e.clientX, + y = e.clientY; + if (x < elementRect.left || x > elementRect.right || y < elementRect.top || y > elementRect.bottom) { + if (ko.unwrap(valueAccessor())) { + element.close(); + } + } + } + }) + } + } +} diff --git a/src/Samples/Common/ViewModels/FeatureSamples/ModalDialog/ModalDialogViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/ModalDialog/ModalDialogViewModel.cs new file mode 100644 index 0000000000..f0181260aa --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/ModalDialog/ModalDialogViewModel.cs @@ -0,0 +1,42 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Net.Http.Headers; +using System.Text; +using DotVVM.Framework.ViewModel; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.ModalDialog +{ + public class ModalDialogViewModel : DotvvmViewModelBase + { + public bool Dialog1Shown { get; set; } + public bool DialogChained1Shown { get; set; } + public bool DialogChained2Shown { get; set; } + public bool CloseEventDialogShown { get; set; } + + public int? NullableIntController { get; set; } + public string NullableStringController { get; set; } + + public DialogModel DialogWithModel { get; set; } = null; + + public int CloseEventCounter { get; set; } = 0; + + public void ShowDialogWithModel() + { + DialogWithModel = new DialogModel() { Property = "Hello" }; + } + + public void CloseDialogWithEvent() + { + CloseEventDialogShown = false; + } + + public class DialogModel + { + public string Property { get; set; } + } + } + +} diff --git a/src/Samples/Common/Views/FeatureSamples/ModalDialog/ModalDialog.dothtml b/src/Samples/Common/Views/FeatureSamples/ModalDialog/ModalDialog.dothtml new file mode 100644 index 0000000000..24247674e1 --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/ModalDialog/ModalDialog.dothtml @@ -0,0 +1,79 @@ +@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.ModalDialog.ModalDialogViewModel + + + + + + + + + + +

Modal dialogs

+ +

+ + + + + + +

+

+ Close events: +

+ + +
+ This is a simple modal dialog, close it by pressing ESC or clicking the button. +
+
+ + +

This is the first chained modal dialog.

+
+ + + +
+ + +

This is the second chained modal dialog.

+ +
+ + + Closing the dialog will increase the counter. Either +
    +
  • Click the backdrop
  • +
  • Press ESC
  • +
  • Use staticCommand
  • +
  • Use command
  • +
  • +
+
+ + +

Edit this field:

+

+ +

+

+
+ + + the number: +
+
+ + + the string: +
+
+ + + diff --git a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs index b7a57dbe57..6b287d9ae9 100644 --- a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs +++ b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs @@ -297,6 +297,7 @@ public partial class SamplesRouteUrls public const string FeatureSamples_MarkupControl_ResourceBindingInControlProperty = "FeatureSamples/MarkupControl/ResourceBindingInControlProperty"; public const string FeatureSamples_MarkupControl_StaticCommandInMarkupControl = "FeatureSamples/MarkupControl/StaticCommandInMarkupControl"; public const string FeatureSamples_MarkupControl_StaticCommandInMarkupControlCallingRegularCommand = "FeatureSamples/MarkupControl/StaticCommandInMarkupControlCallingRegularCommand"; + public const string FeatureSamples_ModalDialog_ModalDialog = "FeatureSamples/ModalDialog/ModalDialog"; public const string FeatureSamples_NestedMasterPages_Content = "FeatureSamples/NestedMasterPages/Content"; public const string FeatureSamples_NoJsForm_NoJsForm = "FeatureSamples/NoJsForm/NoJsForm"; public const string FeatureSamples_ParameterBinding_OptionalParameterBinding = "FeatureSamples/ParameterBinding/OptionalParameterBinding"; diff --git a/src/Samples/Tests/Tests/Feature/ModalDialogTests.cs b/src/Samples/Tests/Tests/Feature/ModalDialogTests.cs new file mode 100644 index 0000000000..ccd5c038d0 --- /dev/null +++ b/src/Samples/Tests/Tests/Feature/ModalDialogTests.cs @@ -0,0 +1,112 @@ +using System; +using System.Linq; +using DotVVM.Samples.Tests.Base; +using DotVVM.Testing.Abstractions; +using OpenQA.Selenium; +using OpenQA.Selenium.Interactions; +using Riganti.Selenium.Core; +using Riganti.Selenium.Core.Abstractions; +using Riganti.Selenium.Core.Api; +using Riganti.Selenium.DotVVM; +using Xunit; + +namespace DotVVM.Samples.Tests.Feature +{ + public class ModalDialogTests : AppSeleniumTest + { + public ModalDialogTests(Xunit.Abstractions.ITestOutputHelper output) : base(output) + { + } + + IElementWrapper OpenDialog(IBrowserWrapper browser, string dialogId) + { + var button = browser.Single($"btn-open-{dialogId}", SelectByDataUi); + AssertUI.IsNotDisplayed(browser.Single(dialogId, SelectByDataUi)); + button.Click(); + AssertUI.HasClass(browser.Single($"btn-open-{dialogId}", SelectByDataUi), "button-active"); + var dialog = browser.Single(dialogId, SelectByDataUi); + AssertUI.IsDisplayed(dialog); + return dialog; + } + + void CheckDialogCloses(IBrowserWrapper browser, string id, Action closeAction) + { + var dialog = OpenDialog(browser, id); + closeAction(dialog); + AssertUI.IsNotDisplayed(browser.Single(id, SelectByDataUi)); + AssertUI.HasNotClass(browser.Single($"btn-open-{id}", SelectByDataUi), "button-active"); + } + + [Fact] + public void Feature_ModalDialog_Simple() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_ModalDialog_ModalDialog); + CheckDialogCloses(browser, "simple", dialog => dialog.Single("btn-close", SelectByDataUi).Click()); + CheckDialogCloses(browser, "simple", dialog => { + // backdrop click does nothing + new Actions(browser.Driver).MoveToLocation(1, 1).Click().Perform(); + AssertUI.IsDisplayed(dialog); + + dialog.SendKeys(Keys.Escape); + AssertUI.IsNotDisplayed(dialog); + }); + + }); + } + + [Fact] + public void Feature_ModalDialog_Chained() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_ModalDialog_ModalDialog); + var dialog1 = OpenDialog(browser, "chained1"); + dialog1.Single("btn-next", SelectByDataUi).Click(); + AssertUI.IsNotDisplayed(dialog1); + var dialog2 = browser.Single("chained2", SelectByDataUi); + AssertUI.IsDisplayed(dialog2); + dialog2.Single("btn-close", SelectByDataUi).Click(); + AssertUI.IsNotDisplayed(dialog2); + }); + } + + [Fact] + public void Feature_ModalDialog_CloseEvent() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_ModalDialog_ModalDialog); + + CheckDialogCloses(browser, "close-event", dialog => dialog.Single("btn-close1", SelectByDataUi).Click()); + AssertUI.InnerTextEquals(browser.Single("close-event-counter", SelectByDataUi), "1"); + CheckDialogCloses(browser, "close-event", dialog => dialog.Single("btn-close2", SelectByDataUi).Click()); + AssertUI.InnerTextEquals(browser.Single("close-event-counter", SelectByDataUi), "2"); + CheckDialogCloses(browser, "close-event", dialog => dialog.Single("btn-close3", SelectByDataUi).Click()); + AssertUI.InnerTextEquals(browser.Single("close-event-counter", SelectByDataUi), "3"); + CheckDialogCloses(browser, "close-event", dialog => dialog.SendKeys(Keys.Escape)); + AssertUI.InnerTextEquals(browser.Single("close-event-counter", SelectByDataUi), "4"); + + CheckDialogCloses(browser, "close-event", dialog => { + // dialog click + new Actions(browser.Driver).MoveToElement(dialog.WebElement, 1, 1).Click().Perform(); + AssertUI.IsDisplayed(dialog); + // backdrop click + new Actions(browser.Driver).MoveToLocation(1, 1).Click().Perform(); + }); + }); + } + + [Fact] + public void Feature_ModalDialog_ModelController() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_ModalDialog_ModalDialog); + CheckDialogCloses(browser, "view-model", dialog => dialog.Single("btn-save", SelectByDataUi).Click()); + CheckDialogCloses(browser, "view-model", dialog => dialog.Single("btn-close", SelectByDataUi).Click()); + CheckDialogCloses(browser, "int", dialog => dialog.Single("btn-close", SelectByDataUi).Click()); + // clearing the numeric input puts null into the nullable integer controller + CheckDialogCloses(browser, "int", dialog => dialog.Single("editor", SelectByDataUi).Clear()); + CheckDialogCloses(browser, "string", dialog => dialog.Single("btn-close", SelectByDataUi).Click()); + }); + } + } +} From 479cb57e1ab549c8794b072e4824352188d0b937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 23 Feb 2024 14:04:50 +0100 Subject: [PATCH 2/5] ModalDialog Close only fires if closed by the user --- src/Framework/Framework/Controls/ModalDialog.cs | 7 ++++--- .../Scripts/binding-handlers/modal-dialog.ts | 7 ++++--- .../FeatureSamples/ModalDialog/ModalDialog.dothtml | 6 +++--- .../Tests/Tests/Feature/ModalDialogTests.cs | 14 ++++++++------ 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/Framework/Framework/Controls/ModalDialog.cs b/src/Framework/Framework/Controls/ModalDialog.cs index a81a6e497f..f5d2415062 100644 --- a/src/Framework/Framework/Controls/ModalDialog.cs +++ b/src/Framework/Framework/Controls/ModalDialog.cs @@ -43,7 +43,7 @@ public bool CloseOnBackdropClick public static readonly DotvvmProperty CloseOnBackdropClickProperty = DotvvmProperty.Register(nameof(CloseOnBackdropClick), false); - /// Triggered when the dialog is closed. Called regardless if it was closed by user input or by change. + /// Triggered when the dialog is closed. Called only if it was closed by user input, not by change. public Command? Close { get { return (Command?)GetValue(CloseProperty); } @@ -67,12 +67,13 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest if (GetValueOrBinding(CloseOnBackdropClickProperty) is {} x && !x.ValueEquals(false)) { - writer.AddKnockoutDataBind("dotvvm-model-backdrop-close", x.GetJsExpression(this)); + writer.AddKnockoutDataBind("dotvvm-modal-backdrop-close", x.GetJsExpression(this)); } if (GetCommandBinding(CloseProperty) is {} close) { - writer.AddAttribute("onclose", KnockoutHelper.GenerateClientPostBackScript(nameof(Close), close, this, returnValue: null)); + var postbackScript = KnockoutHelper.GenerateClientPostBackScript(nameof(Close), close, this, returnValue: null); + writer.AddAttribute("onclose", "if (event.target.returnValue!=\"_dotvvm_modal_supress_onclose\") {" + postbackScript + "}"); } base.AddAttributesToRender(writer, context); diff --git a/src/Framework/Framework/Resources/Scripts/binding-handlers/modal-dialog.ts b/src/Framework/Framework/Resources/Scripts/binding-handlers/modal-dialog.ts index b745237de9..9d25f3a0c1 100644 --- a/src/Framework/Framework/Resources/Scripts/binding-handlers/modal-dialog.ts +++ b/src/Framework/Framework/Resources/Scripts/binding-handlers/modal-dialog.ts @@ -14,18 +14,19 @@ export default { shouldOpen = value != null && value !== false; if (shouldOpen != element.open) { if (shouldOpen) { + element.returnValue = "" // reset returnValue, ESC key leaves the old return value element.showModal() } else { - element.close() + element.close("_dotvvm_modal_supress_onclose") } } }, }, - "dotvvm-model-backdrop-close": { + "dotvvm-modal-backdrop-close": { init(element: HTMLDialogElement, valueAccessor: () => any) { // closes the dialog when the backdrop is clicked element.addEventListener("click", (e) => { - if (e.target == element) { + if (e.target == element) { const elementRect = element.getBoundingClientRect(), x = e.clientX, y = e.clientY; diff --git a/src/Samples/Common/Views/FeatureSamples/ModalDialog/ModalDialog.dothtml b/src/Samples/Common/Views/FeatureSamples/ModalDialog/ModalDialog.dothtml index 24247674e1..ac0188eb3a 100644 --- a/src/Samples/Common/Views/FeatureSamples/ModalDialog/ModalDialog.dothtml +++ b/src/Samples/Common/Views/FeatureSamples/ModalDialog/ModalDialog.dothtml @@ -51,9 +51,9 @@
  • Click the backdrop
  • Press ESC
  • -
  • Use staticCommand
  • -
  • Use command
  • -
  • +
  • Use staticCommand
  • +
  • Use command
  • +
diff --git a/src/Samples/Tests/Tests/Feature/ModalDialogTests.cs b/src/Samples/Tests/Tests/Feature/ModalDialogTests.cs index ccd5c038d0..5d9a94fdf9 100644 --- a/src/Samples/Tests/Tests/Feature/ModalDialogTests.cs +++ b/src/Samples/Tests/Tests/Feature/ModalDialogTests.cs @@ -76,14 +76,14 @@ public void Feature_ModalDialog_CloseEvent() RunInAllBrowsers(browser => { browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_ModalDialog_ModalDialog); - CheckDialogCloses(browser, "close-event", dialog => dialog.Single("btn-close1", SelectByDataUi).Click()); + CheckDialogCloses(browser, "close-event", dialog => dialog.Single("btn-close-staticcommand", SelectByDataUi).Click()); + AssertUI.InnerTextEquals(browser.Single("close-event-counter", SelectByDataUi), "0"); + CheckDialogCloses(browser, "close-event", dialog => dialog.Single("btn-close-command", SelectByDataUi).Click()); + AssertUI.InnerTextEquals(browser.Single("close-event-counter", SelectByDataUi), "0"); + CheckDialogCloses(browser, "close-event", dialog => dialog.Single("btn-close-form", SelectByDataUi).Click()); AssertUI.InnerTextEquals(browser.Single("close-event-counter", SelectByDataUi), "1"); - CheckDialogCloses(browser, "close-event", dialog => dialog.Single("btn-close2", SelectByDataUi).Click()); - AssertUI.InnerTextEquals(browser.Single("close-event-counter", SelectByDataUi), "2"); - CheckDialogCloses(browser, "close-event", dialog => dialog.Single("btn-close3", SelectByDataUi).Click()); - AssertUI.InnerTextEquals(browser.Single("close-event-counter", SelectByDataUi), "3"); CheckDialogCloses(browser, "close-event", dialog => dialog.SendKeys(Keys.Escape)); - AssertUI.InnerTextEquals(browser.Single("close-event-counter", SelectByDataUi), "4"); + AssertUI.InnerTextEquals(browser.Single("close-event-counter", SelectByDataUi), "2"); CheckDialogCloses(browser, "close-event", dialog => { // dialog click @@ -91,7 +91,9 @@ public void Feature_ModalDialog_CloseEvent() AssertUI.IsDisplayed(dialog); // backdrop click new Actions(browser.Driver).MoveToLocation(1, 1).Click().Perform(); + }); + AssertUI.InnerTextEquals(browser.Single("close-event-counter", SelectByDataUi), "3"); }); } From 2bf47a1b3c6d73748b5a4682158d9945ceb5fb14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 23 Feb 2024 14:21:04 +0100 Subject: [PATCH 3/5] ModalDialog: commit new serialized configuration --- .../Framework/Controls/ModalDialog.cs | 6 +++--- ...ializationTests.SerializeDefaultConfig.json | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/Framework/Framework/Controls/ModalDialog.cs b/src/Framework/Framework/Controls/ModalDialog.cs index f5d2415062..c1bbf9d1c7 100644 --- a/src/Framework/Framework/Controls/ModalDialog.cs +++ b/src/Framework/Framework/Controls/ModalDialog.cs @@ -26,13 +26,13 @@ public ModalDialog() /// A value indicating whether the dialog is open. The value can either be a boolean or an object (not false or not null -> shown). On close, the value is written back into the Open binding. [MarkupOptions(AllowHardCodedValue = false)] - public object Open + public object? Open { - get { return (bool?)GetValue(OpenProperty) ?? false; } + get { return GetValue(OpenProperty); } set { SetValue(OpenProperty, value); } } public static readonly DotvvmProperty OpenProperty = - DotvvmProperty.Register(nameof(Open), false); + DotvvmProperty.Register(nameof(Open), null); /// Add an event handler which closes the dialog when the backdrop is clicked. public bool CloseOnBackdropClick diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index 17991ed4b6..4f5cde2cf5 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -1177,6 +1177,20 @@ "defaultValue": "" } }, + "DotVVM.Framework.Controls.ModalDialog": { + "Close": { + "type": "DotVVM.Framework.Binding.Expressions.Command, DotVVM.Framework", + "isCommand": true + }, + "CloseOnBackdropClick": { + "type": "System.Boolean", + "defaultValue": false + }, + "Open": { + "type": "System.Object", + "onlyBindings": true + } + }, "DotVVM.Framework.Controls.MultiSelector": { "SelectedValues": { "type": "System.Object", @@ -2216,6 +2230,10 @@ "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework", "withoutContent": true }, + "DotVVM.Framework.Controls.ModalDialog": { + "assembly": "DotVVM.Framework", + "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework" + }, "DotVVM.Framework.Controls.MultiSelect": { "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.MultiSelectHtmlControlBase, DotVVM.Framework" From 6158f457f0a90603677d2326070caf6fd7460eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 24 Feb 2024 12:50:10 +0100 Subject: [PATCH 4/5] ModalDialog: fix UI tests in Chrome --- src/Samples/Tests/Tests/Feature/ModalDialogTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Samples/Tests/Tests/Feature/ModalDialogTests.cs b/src/Samples/Tests/Tests/Feature/ModalDialogTests.cs index 5d9a94fdf9..5984d245ea 100644 --- a/src/Samples/Tests/Tests/Feature/ModalDialogTests.cs +++ b/src/Samples/Tests/Tests/Feature/ModalDialogTests.cs @@ -87,7 +87,8 @@ public void Feature_ModalDialog_CloseEvent() CheckDialogCloses(browser, "close-event", dialog => { // dialog click - new Actions(browser.Driver).MoveToElement(dialog.WebElement, 1, 1).Click().Perform(); + this.TestOutput.WriteLine($"Dialog location: {dialog.WebElement.Location}"); + new Actions(browser.Driver).MoveToLocation(dialog.WebElement.Location.X + 10, dialog.WebElement.Location.Y + 10).Click().Perform(); AssertUI.IsDisplayed(dialog); // backdrop click new Actions(browser.Driver).MoveToLocation(1, 1).Click().Perform(); From a473fd15aad52b6cbe83941c9a40ad25757e3595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Mon, 26 Feb 2024 11:05:12 +0100 Subject: [PATCH 5/5] Fix Compilation page flaky UI test --- .../Tests/Feature/CompilationPageTests.cs | 13 ++-- .../Tests/Feature/PostbackConcurrencyTests.cs | 61 ++++++++++--------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/Samples/Tests/Tests/Feature/CompilationPageTests.cs b/src/Samples/Tests/Tests/Feature/CompilationPageTests.cs index a408d3b97e..e5823f40a3 100644 --- a/src/Samples/Tests/Tests/Feature/CompilationPageTests.cs +++ b/src/Samples/Tests/Tests/Feature/CompilationPageTests.cs @@ -19,15 +19,18 @@ public void Feature_CompilationPage_SmokeTest() { RunInAllBrowsers(browser => { browser.NavigateToUrl("/_dotvvm/diagnostics/compilation"); - browser.Single("compile-all-button", By.Id).Click(); + browser.WaitFor(() => { browser.Single("compile-all-button", By.Id).Click(); }, timeout: 15_000); browser.Single("Routes", SelectByButtonText).Click(); // shows failed pages - Assert.InRange(browser.FindElements("tbody tr.success").Count, 10, int.MaxValue); - Assert.InRange(browser.FindElements("tbody tr.failure").Count, 10, int.MaxValue); browser.WaitFor(() => { - AssertUI.HasClass(TableRow(browser, "FeatureSamples_CompilationPage_BindingsTestError"), "failure", waitForOptions: WaitForOptions.Disabled); - }, timeout: 10_000); + Assert.InRange(browser.FindElements("tbody tr.success").Count, 10, int.MaxValue); + Assert.InRange(browser.FindElements("tbody tr.failure").Count, 10, int.MaxValue); + var failedRow = () => TableRow(browser, "FeatureSamples_CompilationPage_BindingsTestError"); + AssertUI.InnerTextEquals(failedRow().ElementAt("td", 1), "FeatureSamples/CompilationPage/BindingsTestError"); + AssertUI.InnerTextEquals(failedRow().ElementAt("td", 3), "CompilationFailed"); + AssertUI.HasClass(failedRow(), "failure", waitForOptions: WaitForOptions.Disabled); + }, timeout: 60_000); AssertUI.HasNotClass(TableRow(browser, "FeatureSamples_CompilationPage_BindingsTest"), "failure"); // shows some errors and warnings diff --git a/src/Samples/Tests/Tests/Feature/PostbackConcurrencyTests.cs b/src/Samples/Tests/Tests/Feature/PostbackConcurrencyTests.cs index 16a7a559a5..d7f2d84662 100644 --- a/src/Samples/Tests/Tests/Feature/PostbackConcurrencyTests.cs +++ b/src/Samples/Tests/Tests/Feature/PostbackConcurrencyTests.cs @@ -156,16 +156,17 @@ public void Feature_PostbackConcurrency_StressTest_Default() browser.ElementAt("input[type=button]", 1).Click(); - Thread.Sleep(10000); - var before = int.Parse(browser.Single(".result-before").GetInnerText().Trim()); - var rejected = int.Parse(browser.Single(".result-rejected").GetInnerText().Trim()); - var after = int.Parse(browser.Single(".result-after").GetInnerText().Trim()); - var value = int.Parse(browser.Single(".result-value").GetInnerText().Trim()); - - Assert.True(0 < value && value <= 100); - Assert.Equal(100, before); - Assert.Equal(100, after); - Assert.Equal(0, rejected); + browser.WaitFor(() => { + var before = int.Parse(browser.Single(".result-before").GetInnerText().Trim()); + var rejected = int.Parse(browser.Single(".result-rejected").GetInnerText().Trim()); + var after = int.Parse(browser.Single(".result-after").GetInnerText().Trim()); + var value = int.Parse(browser.Single(".result-value").GetInnerText().Trim()); + + Assert.InRange(value, 1, 100); + Assert.Equal(100, before); + Assert.Equal(100, after); + Assert.Equal(0, rejected); + }, timeout: 30_000); }); } @@ -178,15 +179,16 @@ public void Feature_PostbackConcurrency_StressTest_Deny() browser.ElementAt("input[type=button]", 3).Click(); - Thread.Sleep(10000); - var before = int.Parse(browser.Single(".result-before").GetInnerText().Trim()); - var rejected = int.Parse(browser.Single(".result-rejected").GetInnerText().Trim()); - var after = int.Parse(browser.Single(".result-after").GetInnerText().Trim()); - var value = int.Parse(browser.Single(".result-value").GetInnerText().Trim()); - - Assert.True(0 < value && value <= 100); - Assert.Equal(100, before + rejected); - Assert.Equal(100, after); + browser.WaitFor(() => { + var before = int.Parse(browser.Single(".result-before").GetInnerText().Trim()); + var rejected = int.Parse(browser.Single(".result-rejected").GetInnerText().Trim()); + var after = int.Parse(browser.Single(".result-after").GetInnerText().Trim()); + var value = int.Parse(browser.Single(".result-value").GetInnerText().Trim()); + + Assert.InRange(value, 1, 100); + Assert.Equal(100, before + rejected); + Assert.Equal(100, after); + }, timeout: 30_000); }); } @@ -199,16 +201,17 @@ public void Feature_PostbackConcurrency_StressTest_Queue() browser.ElementAt("input[type=button]", 5).Click(); - Thread.Sleep(10000); - var before = int.Parse(browser.Single(".result-before").GetInnerText().Trim()); - var rejected = int.Parse(browser.Single(".result-rejected").GetInnerText().Trim()); - var after = int.Parse(browser.Single(".result-after").GetInnerText().Trim()); - var value = int.Parse(browser.Single(".result-value").GetInnerText().Trim()); - - Assert.Equal(100, value); - Assert.Equal(100, before); - Assert.Equal(100, after); - Assert.Equal(0, rejected); + browser.WaitFor(() => { + var before = int.Parse(browser.Single(".result-before").GetInnerText().Trim()); + var rejected = int.Parse(browser.Single(".result-rejected").GetInnerText().Trim()); + var after = int.Parse(browser.Single(".result-after").GetInnerText().Trim()); + var value = int.Parse(browser.Single(".result-value").GetInnerText().Trim()); + + Assert.Equal(100, value); + Assert.Equal(100, before); + Assert.Equal(100, after); + Assert.Equal(0, rejected); + }, timeout: 30_000); }); }