// Copyright Keysight Technologies 2012-2019 // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, you can obtain one at http://mozilla.org/MPL/2.0/. using System; using System.ComponentModel; using System.Xml.Linq; using OpenTap.Translation; namespace OpenTap.Plugins.BasicSteps { public enum InputButtons { [Display("OK / Cancel", "Shows the OK and Cancel buttons")] OkCancel, [Display("Yes / No", "Shows the Yes and No buttons")] YesNo } enum WaitForInputResult1 { // The order of the results determines the order in which the buttons is shown in the dialog box. // The number assigned, determines the default value. No = 2, Yes = 1 } enum WaitForInputResult2 { // The order of the results determines the order in which the buttons is shown in the dialog box. // The number assigned, determines the default value. Cancel = 2, [Display("OK")] Ok = 1 } class DialogRequest : IDisplayAnnotation, IStringLocalizer { public DialogRequest(string Title, string Message) { this.Name = Title; this.Message = Message; } [Browsable(false)] public bool PictureEnabled => Picture != null; [Layout(LayoutMode.FullRow)] [Display("Picture", Order: 1)] [EnabledIf(nameof(PictureEnabled), HideIfDisabled = true)] public Picture Picture { get; set; } // implementing Name of IDisplayAnnotation explicitly. public string Name { get;} [Layout(LayoutMode.FullRow, rowHeight: 2)] [Browsable(true)] [Display("Message", Order: 2)] public string Message { get; } [Browsable(false)] public InputButtons Buttons { get; set; } [Layout(LayoutMode.FloatBottom | LayoutMode.FullRow)] [EnabledIf("Buttons", InputButtons.YesNo, HideIfDisabled = true)] [Submit] public WaitForInputResult1 Input1 { get; set; } = WaitForInputResult1.Yes; [Layout(LayoutMode.FloatBottom | LayoutMode.FullRow)] [EnabledIf("Buttons", InputButtons.OkCancel, HideIfDisabled = true)] [Submit] public WaitForInputResult2 Input2 { get; set; } = WaitForInputResult2.Ok; // the rest of IDisplayAnntation can be implemented implicitly // as these properties are not really needed for user input requests. string IDisplayAnnotation.Description => string.Empty; string[] IDisplayAnnotation.Group => Array.Empty(); double IDisplayAnnotation.Order => DisplayAttribute.DefaultOrder; bool IDisplayAnnotation.Collapsed => false; } [Display("Dialog", Group: "OpenTap", Description: "Shows a message to the user and waits for a response. " + "A verdict can be set based on the response.",Order:1)] public class DialogStep : TestStep { [Display("Message", Description: "The message shown to the user.", Order: 0.1)] [Layout(LayoutMode.Normal, 2, maxRowHeight: 5)] public string Message { get; set; } [Display("Title", "The title of the dialog box.", Order: 0)] public string Title { get; set; } [Display("Buttons", "Selects what buttons the user gets presented with.", Order: 0.2)] public InputButtons Buttons { get; set; } [Display("If Yes/OK", "This is the verdict presented when the user clicks \"Yes\" or \"OK\".", Group: "Verdict", Order: 0.3, Collapsed: true)] public Verdict PositiveAnswer { get; set; } [EnabledIf("Buttons", InputButtons.OkCancel, InputButtons.YesNo)] [Display("If No/Cancel", "This is the verdict presented when the user clicks \"No\" or \"Cancel\".", Group: "Verdict", Order: 0.4, Collapsed: true)] public Verdict NegativeAnswer { get; set; } [Display("Use Timeout", "Enabling this will make the dialog close as if No/Cancel was clicked after an amount of time.", Group: "Timeout", Order: 1, Collapsed: true)] public bool UseTimeout { get; set; } double timeout = 5; [EnabledIf("UseTimeout", true)] [Unit("s")] [Display("Timeout", "After this time the dialog will return the default answer.", Group: "Timeout", Order: 2, Collapsed: true)] public double Timeout { get { return timeout; } set { if (value >= 0) timeout = value; else throw new Exception("Timeout must be greater than 0 seconds."); } } [EnabledIf(nameof(UseTimeout), true)] [Display("Default Verdict", "The verdict the step will have if timeout is reached", Group: "Timeout", Order: 1, Collapsed: true)] public Verdict DefaultAnswer { get; set; } [Display("Show Picture", "The dialog will include a picture if the environment supports it.", "Picture", Order: 0, Collapsed: true)] public bool ShowPicture { get; set; } [EnabledIf(nameof(ShowPicture), HideIfDisabled = true)] public Picture Picture { get; } = new Picture(); [Display("Source", "The source of the picture. Can be a URL or a file path.", "Picture", Order: 2, Collapsed: true)] [FilePath(FilePathAttribute.BehaviorChoice.Open)] [EnabledIf(nameof(ShowPicture), HideIfDisabled = true)] public string PictureSource { get => Picture.Source; set => Picture.Source = value; } [Display("Description", "A description of the picture. This is usually displayed in a mouseover tooltip, or if the picture fails to load.", "Picture", Order: 3, Collapsed: true)] [EnabledIf(nameof(ShowPicture), HideIfDisabled = true)] public string PictureDescription { get => Picture.Description; set => Picture.Description = value; } public DialogStep() { Message = "Message"; Title = "Title"; Rules.Add(() => !string.IsNullOrWhiteSpace(Title), "Title is empty", Title); Rules.Add(() => !string.IsNullOrWhiteSpace(Message), "Message is empty", Message); } public override void Run() { Verdict answer = DefaultAnswer; var req = new DialogRequest(Title, Message) { Buttons = Buttons, Picture = ShowPicture ? Picture : null }; try { var timeout = TimeSpan.FromSeconds(Timeout); if (timeout == TimeSpan.Zero) timeout = TimeSpan.FromSeconds(0.001); if (UseTimeout == false) timeout = TimeSpan.MaxValue; UserInput.Request(req, timeout, false); if (Buttons == InputButtons.OkCancel) { answer = req.Input2 == WaitForInputResult2.Ok ? PositiveAnswer : NegativeAnswer; } else { answer = req.Input1 == WaitForInputResult1.Yes ? PositiveAnswer : NegativeAnswer; } } catch (TimeoutException) { } Verdict = answer; } } class DialogStepCompatibilitSerializer : ITapSerializerPlugin { public double Order => 2; // Box: // A place to put a value. // Since the step is not available when the property is deserialized. // we create a box, where the step value can be put at a later point. // then in Defer we get the step inside the box. class Box { public DialogStep Step; } static readonly string legacyNotSetName = "Not_Set"; static readonly XName legacyDefaultAnswerPropertyName = "DefaultAnswer"; static readonly XName TestStepName = nameof(TestStep); Box currentBox; public bool Deserialize(XElement node, ITypeData t, Action setter) { if (t.IsA(typeof(Verdict))) { if(node.Value == legacyNotSetName) { node.SetValue(nameof(Verdict.NotSet)); return TapSerializer.GetCurrentSerializer().Serialize(node, setter, t); } } if(node.Name == TestStepName && t.DescendsTo(typeof(DialogStep))) { if (currentBox != null) return false; currentBox = new Box(); var serializer = TapSerializer.GetCurrentSerializer(); return serializer.Deserialize(node, x => { currentBox.Step = (DialogStep)x; setter(x); }, t); } else if(currentBox != null && node.Name == legacyDefaultAnswerPropertyName) { if(bool.TryParse(node.Value, out bool defaultAnswer)) { node.Remove(); var serializer = TapSerializer.GetCurrentSerializer(); var thisbox = currentBox; serializer.DeferLoad(() => { if (thisbox.Step == null) return; if (defaultAnswer) { thisbox.Step.DefaultAnswer = thisbox.Step.PositiveAnswer; } else { thisbox.Step.DefaultAnswer = thisbox.Step.NegativeAnswer; } }); return true; } } return false; } public bool Serialize(XElement node, object obj, ITypeData expectedType) { return false; } } }