using Artemis.Core.Internal; using Artemis.Core.Tests.Infrastructure; using Artemis.Core.Tests.Infrastructure.Plugins; using Artemis.Core.Tests.Infrastructure.Plugins.LayerBrushes; using Artemis.Core.Tests.Infrastructure.Plugins.Modules; using Artemis.Core.Tests.Infrastructure.Plugins.Nodes; using Artemis.Core.Tests.Infrastructure.Rendering; using Artemis.Core.Tests.Infrastructure.VisualScripting; using Artemis.Storage.Entities.Profile.Conditions; using RGB.NET.Core; using SkiaSharp; namespace Artemis.Core.Tests.Conditions; /// /// Event conditions on a layer on the A key, with a main segment of one second and no start or end segment /// public class EventConditionTests : RenderTestBase { private readonly TestModule _module; private readonly DataModelTestNodeProvider _nodes; public EventConditionTests() { TestPlugin plugin = Track(new TestPlugin()); _module = plugin.AddFeature(new TestModule()); _nodes = plugin.AddFeature(new DataModelTestNodeProvider()); } private TestDataModel DataModel => _module.DataModel; #region Play [Fact] public void Layer_StaysOffUntilTheEventTriggers() { (Layer layer, _) = AddLayer(); using RenderFrame frame = Render(layer.Profile); layer.DisplayConditionMet.ShouldBeFalse(); ShouldBeOff(frame); } [Fact] public void Play_PlaysTheTimelineOnceWhenTheEventTriggers() { (Layer layer, EventCondition condition) = AddLayer(); DataModel.Event.Trigger(); using RenderFrame triggered = Render(layer.Profile); bool metWhenTriggered = condition.IsMet; using RenderFrame playing = Render(layer.Profile, 0.5); bool metWhilePlaying = condition.IsMet; using RenderFrame finished = Render(layer.Profile, 0.5); // The condition is only met in the frame the event triggered, the timeline then plays to the end metWhenTriggered.ShouldBeTrue(); metWhilePlaying.ShouldBeFalse(); ShouldBeOn(triggered); ShouldBeOn(playing); ShouldBeOff(finished); layer.Timeline.IsFinished.ShouldBeTrue(); } [Fact] public void Play_TriggeringTwiceBetweenFrames_PlaysOnce() { (Layer layer, _) = AddLayer(overlapMode: EventOverlapMode.Restart); DataModel.Event.Trigger(); DataModel.Event.Trigger(); using (Render(layer.Profile, 0.5)) { } using RenderFrame frame = Render(layer.Profile, 0.6); ShouldBeOff(frame); } [Fact] public void Play_EventTriggeredBeforeTheConditionExisted_StillPlaysTheLayer() { DataModel.Event.Trigger(); (Layer layer, _) = AddLayer(); using RenderFrame frame = Render(layer.Profile); // Pinned as-is: the condition starts out having seen no triggers at all, so a trigger from before it was created // (for example long before the profile was activated) still counts as new and plays the layer once. Looks like // the condition should only react to triggers that happen after it started watching. ShouldBeOn(frame); } [Fact] public void Play_Restart_StartsOverWhenTheEventTriggersAgain() { (Layer layer, _) = AddLayer(overlapMode: EventOverlapMode.Restart); DataModel.Event.Trigger(); using (Render(layer.Profile, 0.6)) { } DataModel.Event.Trigger(); using (Render(layer.Profile)) { } layer.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(0.1)); using RenderFrame frame = Render(layer.Profile, 0.5); ShouldBeOn(frame); } [Fact] public void Play_Ignore_KeepsPlayingWhenTheEventTriggersAgain() { (Layer layer, _) = AddLayer(overlapMode: EventOverlapMode.Ignore); DataModel.Event.Trigger(); using (Render(layer.Profile, 0.6)) { } DataModel.Event.Trigger(); using (Render(layer.Profile)) { } layer.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(0.7)); using RenderFrame frame = Render(layer.Profile, 0.4); ShouldBeOff(frame); } [Fact] public void Play_Ignore_PlaysAgainOnceTheTimelineFinished() { (Layer layer, _) = AddLayer(overlapMode: EventOverlapMode.Ignore); DataModel.Event.Trigger(); using (Render(layer.Profile, 1.5)) { } DataModel.Event.Trigger(); using RenderFrame frame = Render(layer.Profile); layer.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(0.1)); ShouldBeOn(frame); } [Fact] public void Play_Copy_PlaysACopyOnTopWhenTheEventTriggersAgain() { // The color goes from red to blue over the second, so the color tells how far along a timeline is (Layer layer, _) = AddLayer(overlapMode: EventOverlapMode.Copy, fadeToBlue: true); // Copies are created from what was last saved Harness.Save(layer.Profile); DataModel.Event.Trigger(); using RenderFrame first = Render(layer.Profile, 0.8); DataModel.Event.Trigger(); using RenderFrame copied = Render(layer.Profile); TimeSpan positionWhenCopied = layer.Timeline.Position; using RenderFrame originalFinished = Render(layer.Profile, 0.2); using RenderFrame copyFinished = Render(layer.Profile, 0.8); first.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(ColorAt(0.8)); // The original is at 0.9 seconds, the copy is drawn on top at 0.1 seconds positionWhenCopied.ShouldBe(TimeSpan.FromSeconds(0.9)); copied.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(ColorAt(0.1)); originalFinished.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(ColorAt(0.3)); ShouldBeOff(copyFinished); } [Fact] public void Play_Copy_KeepsTheLayerEnabledWhileACopyPlays() { (Layer layer, _) = AddLayer(overlapMode: EventOverlapMode.Copy); Harness.Save(layer.Profile); DataModel.Event.Trigger(); using (Render(layer.Profile, 0.8)) { } DataModel.Event.Trigger(); using (Render(layer.Profile)) { } using (Render(layer.Profile, 0.5)) { } layer.Timeline.IsFinished.ShouldBeTrue(); layer.Enabled.ShouldBeTrue(); // Once the copy finished it is removed on the next frame and the layer is disabled the frame after using (Render(layer.Profile, 0.5)) { } using (Render(layer.Profile)) { } using (Render(layer.Profile)) { } layer.Enabled.ShouldBeFalse(); } [Fact] public void Play_Copy_OnAFolder_KeepsPlaying() { Profile profile = Harness.CreateProfile(); Folder folder = Harness.AddFolder(profile.GetRootFolder()); Harness.AddLayer(folder, [Led(LedId.Keyboard_A)]); UseEventCondition(folder, "Event", overlapMode: EventOverlapMode.Copy); DataModel.Event.Trigger(); using (Render(profile, 0.6)) { } DataModel.Event.Trigger(); using (Render(profile)) { } // Only layers can be copied, a folder neither copies nor restarts folder.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(0.7)); } #endregion #region Toggle [Fact] public void Toggle_KeepsRepeatingUntilTheEventTriggersAgain() { (Layer layer, EventCondition condition) = AddLayer(EventTriggerMode.Toggle); DataModel.Event.Trigger(); using RenderFrame on = Render(layer.Profile); using RenderFrame stillOn = Render(layer.Profile, 1.5); condition.IsMet.ShouldBeTrue(); ShouldBeOn(on); ShouldBeOn(stillOn); // Wrapped around the main segment rather than finishing layer.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(0.5)); } [Fact] public void Toggle_Finish_FinishesTheCurrentRunWhenToggledOff() { (Layer layer, EventCondition condition) = AddLayer(EventTriggerMode.Toggle, toggleOffMode: EventToggleOffMode.Finish); DataModel.Event.Trigger(); using (Render(layer.Profile, 0.5)) { } DataModel.Event.Trigger(); using RenderFrame toggledOff = Render(layer.Profile); using RenderFrame finished = Render(layer.Profile, 0.5); condition.IsMet.ShouldBeFalse(); ShouldBeOn(toggledOff); ShouldBeOff(finished); } [Fact] public void Toggle_SkipToEnd_StopsRightAwayWhenToggledOff() { (Layer layer, _) = AddLayer(EventTriggerMode.Toggle, toggleOffMode: EventToggleOffMode.SkipToEnd); DataModel.Event.Trigger(); using (Render(layer.Profile, 0.5)) { } DataModel.Event.Trigger(); using RenderFrame toggledOff = Render(layer.Profile); ShouldBeOff(toggledOff); layer.Timeline.IsFinished.ShouldBeTrue(); } [Fact] public void Toggle_TriggeringTwiceBetweenFrames_TogglesOnce() { (Layer layer, EventCondition condition) = AddLayer(EventTriggerMode.Toggle); DataModel.Event.Trigger(); DataModel.Event.Trigger(); using RenderFrame frame = Render(layer.Profile); condition.IsMet.ShouldBeTrue(); ShouldBeOn(frame); } [Fact] public void Toggle_TogglesBackOnAfterBeingToggledOff() { (Layer layer, EventCondition condition) = AddLayer(EventTriggerMode.Toggle); DataModel.Event.Trigger(); using (Render(layer.Profile)) { } DataModel.Event.Trigger(); using (Render(layer.Profile, 1.5)) { } DataModel.Event.Trigger(); using RenderFrame frame = Render(layer.Profile); condition.IsMet.ShouldBeTrue(); layer.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(0.1)); ShouldBeOn(frame); } #endregion #region Event arguments [Fact] public void StartNode_OffersTheEventArguments() { (_, EventCondition condition) = AddLayer(eventPath: "EventWithArgs"); INode startNode = condition.GetStartNode(); startNode.ShouldBeOfType(); IPin value = startNode.Pins.ShouldHaveSingleItem(); value.Name.ShouldBe("Value"); value.Type.ShouldBe(typeof(Numeric)); condition.Script.Nodes.ShouldContain(startNode); } [Fact] public void Script_DecidesWhetherATriggerCounts() { (Layer layer, EventCondition condition) = AddLayer(eventPath: "EventWithArgs"); TestGreaterThanNode greaterThan = RequireValueAbove(condition, 3); DataModel.EventWithArgs.Trigger(new TestEventArgs {Value = 1}); using RenderFrame ignored = Render(layer.Profile); DataModel.EventWithArgs.Trigger(new TestEventArgs {Value = 5}); using RenderFrame counted = Render(layer.Profile); ShouldBeOff(ignored); ShouldBeOn(counted); greaterThan.LastInput.ShouldBe(5); } [Fact] public void Script_IsNotRunWithoutANewTrigger() { (Layer layer, EventCondition condition) = AddLayer(eventPath: "EventWithArgs"); TestGreaterThanNode greaterThan = RequireValueAbove(condition, 3); DataModel.EventWithArgs.Trigger(new TestEventArgs {Value = 1}); using (Render(layer.Profile)) { } // Changing the arguments of the last trigger doesn't make it count DataModel.EventWithArgs.LastEventArguments!.Value = 5; using RenderFrame frame = Render(layer.Profile); ShouldBeOff(frame); greaterThan.LastInput.ShouldBe(1); } [Fact] public void WithoutAScript_EveryTriggerCounts() { (Layer layer, _) = AddLayer(eventPath: "EventWithArgs"); DataModel.EventWithArgs.Trigger(new TestEventArgs {Value = 1}); using RenderFrame frame = Render(layer.Profile); ShouldBeOn(frame); } [Fact] public void WithoutAnEvent_TheLayerNeverPlays() { (Layer layer, EventCondition condition) = AddLayer(); condition.EventPath = null; DataModel.Event.Trigger(); using RenderFrame frame = Render(layer.Profile); condition.IsMet.ShouldBeFalse(); ShouldBeOff(frame); } #endregion #region Values instead of events [Fact] public void PathToAValue_PlaysWhenTheValueChanges() { (Layer layer, EventCondition condition) = AddLayer(eventPath: "Integer"); using (Render(layer.Profile, 1.5)) { } using RenderFrame unchanged = Render(layer.Profile); DataModel.Integer = 5; using RenderFrame changed = Render(layer.Profile); condition.GetStartNode().ShouldBeOfType(); ShouldBeOff(unchanged); ShouldBeOn(changed); } [Fact] public void PathToAValue_PlaysOnTheFirstFrame() { (Layer layer, _) = AddLayer(eventPath: "Integer"); using RenderFrame frame = Render(layer.Profile); // Pinned as-is: the last seen value starts out empty, so the first frame always sees the value as changed (0 is // not empty) and plays the layer even though nothing changed. ShouldBeOn(frame); } [Fact] public void PathToAValue_OffersTheNewAndOldValue() { (_, EventCondition condition) = AddLayer(eventPath: "Integer"); EventConditionValueChangedStartNode startNode = condition.GetStartNode().ShouldBeOfType(); startNode.NewValue.Type.ShouldBe(typeof(Numeric)); startNode.OldValue.Type.ShouldBe(typeof(Numeric)); condition.Script.Nodes.ShouldContain(startNode); condition.Script.Nodes.OfType().ShouldBeEmpty(); } [Fact] public void PathToAValue_LetsTheScriptLookAtTheNewValue() { (Layer layer, EventCondition condition) = AddLayer(eventPath: "Integer"); EventConditionValueChangedStartNode startNode = (EventConditionValueChangedStartNode) condition.GetStartNode(); TestGreaterThanNode greaterThan = _nodes.AddNode(condition.Script); greaterThan.Threshold = 3; startNode.NewValue.ConnectTo(greaterThan.Input); condition.Script.Nodes.OfType>().Single().Input.ConnectTo(greaterThan.Output); using (Render(layer.Profile, 1.5)) { } DataModel.Integer = 1; using RenderFrame ignored = Render(layer.Profile); DataModel.Integer = 5; using RenderFrame counted = Render(layer.Profile); ShouldBeOff(ignored); ShouldBeOn(counted); } #endregion #region Saving and loading [Fact] public void Condition_SurvivesSavingAndLoading() { (Layer layer, _) = AddLayer(EventTriggerMode.Toggle, EventOverlapMode.Ignore, EventToggleOffMode.SkipToEnd, eventPath: "EventWithArgs"); Profile reloaded = Harness.SaveAndReload(layer.Profile); EventCondition condition = reloaded.GetAllLayers().Single().DisplayCondition.ShouldBeOfType(); condition.TriggerMode.ShouldBe(EventTriggerMode.Toggle); condition.OverlapMode.ShouldBe(EventOverlapMode.Ignore); condition.ToggleOffMode.ShouldBe(EventToggleOffMode.SkipToEnd); DataModelPath eventPath = condition.EventPath.ShouldNotBeNull(); eventPath.Path.ShouldBe("EventWithArgs"); eventPath.Target.ShouldBeSameAs(DataModel); condition.GetStartNode().Pins.ShouldHaveSingleItem().Name.ShouldBe("Value"); } [Fact] public void ScriptLookingAtTheEventArguments_SurvivesSavingAndLoading() { (Layer layer, EventCondition condition) = AddLayer(eventPath: "EventWithArgs"); RequireValueAbove(condition, 3); Profile reloaded = Harness.SaveAndReload(layer.Profile); EventCondition reloadedCondition = (EventCondition) reloaded.GetAllLayers().Single().DisplayCondition; DataModel.EventWithArgs.Trigger(new TestEventArgs {Value = 1}); using RenderFrame ignored = Render(reloaded); DataModel.EventWithArgs.Trigger(new TestEventArgs {Value = 5}); using RenderFrame counted = Render(reloaded); reloadedCondition.Script.Nodes.OfType().Single().Threshold.ShouldBe(3); ShouldBeOff(ignored); ShouldBeOn(counted); } [Fact] public void ScriptOfAValuePath_SurvivesSavingAndLoading() { (Layer layer, _) = AddLayer(eventPath: "Integer"); Profile reloaded = Harness.SaveAndReload(layer.Profile); EventCondition condition = (EventCondition) reloaded.GetAllLayers().Single().DisplayCondition; condition.GetStartNode().ShouldBeOfType(); condition.Script.Nodes.ShouldContain(condition.GetStartNode()); } [Fact] public void EmptyScript_IsNotSaved() { (_, EventCondition condition) = AddLayer(); condition.Save(); EventConditionEntity entity = condition.Entity.ShouldBeOfType(); entity.Script.ShouldBeNull(); entity.EventPath.ShouldNotBeNull().Path.ShouldBe("Event"); } [Fact] public void ScriptWithOtherNodes_IsSaved() { (_, EventCondition condition) = AddLayer(); _nodes.AddNode(condition.Script); condition.Save(); ((EventConditionEntity) condition.Entity).Script.ShouldNotBeNull(); } [Fact] public void ConditionWithoutAnEvent_SurvivesSavingAndLoading() { (Layer layer, EventCondition condition) = AddLayer(); condition.EventPath = null; Profile reloaded = Harness.SaveAndReload(layer.Profile); EventCondition reloadedCondition = reloaded.GetAllLayers().Single().DisplayCondition.ShouldBeOfType(); reloadedCondition.EventPath.ShouldBeNull(); reloadedCondition.GetStartNode().ShouldBeOfType(); } #endregion #region Other [Fact] public void OverridingTheTimeline_MovesItToThePosition() { (Layer layer, EventCondition condition) = AddLayer(); condition.OverrideTimeline(TimeSpan.FromSeconds(1.5)); layer.Timeline.IsOverridden.ShouldBeTrue(); layer.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(1.5)); } [Fact] public void OverridingTheTimeline_InToggleMode_WrapsAroundTheMainSegment() { (Layer layer, EventCondition condition) = AddLayer(EventTriggerMode.Toggle); condition.OverrideTimeline(TimeSpan.FromSeconds(1.5)); layer.Timeline.Position.ShouldBe(TimeSpan.FromSeconds(0.5)); } [Fact] public void Condition_DependsOnTheModuleOfTheEventAndTheNodesInItsScript() { (_, EventCondition condition) = AddLayer(eventPath: "EventWithArgs"); RequireValueAbove(condition, 3); condition.GetFeatureDependencies().ShouldContain(_module); condition.GetFeatureDependencies().ShouldContain(_nodes); } [Fact] public void Condition_UsesItsScriptAsNodeScript() { (Layer layer, EventCondition condition) = AddLayer(); condition.NodeScript.ShouldBeSameAs(condition.Script); condition.ProfileElement.ShouldBeSameAs(layer); condition.Script.Name.ShouldBe("Activate Layer"); } [Fact] public void DisposingTheCondition_DisposesTheEventPath() { (_, EventCondition condition) = AddLayer(); DataModelPath eventPath = condition.EventPath!; condition.Dispose(); eventPath.IsValid.ShouldBeFalse(); DataModel.ActivePaths.ShouldNotContain(eventPath); } [Fact] public void ChangingASetting_RaisesPropertyChanged() { (_, EventCondition condition) = AddLayer(); List changed = []; condition.PropertyChanged += (_, e) => changed.Add(e.PropertyName); condition.TriggerMode = EventTriggerMode.Toggle; condition.OverlapMode = EventOverlapMode.Copy; condition.ToggleOffMode = EventToggleOffMode.SkipToEnd; changed.ShouldBe([nameof(EventCondition.TriggerMode), nameof(EventCondition.OverlapMode), nameof(EventCondition.ToggleOffMode)]); } #endregion private (Layer Layer, EventCondition Condition) AddLayer(EventTriggerMode triggerMode = EventTriggerMode.Play, EventOverlapMode overlapMode = EventOverlapMode.Restart, EventToggleOffMode toggleOffMode = EventToggleOffMode.Finish, string eventPath = "Event", bool fadeToBlue = false) { (Layer layer, SolidColorTestBrush brush) = Harness.AddLayer(Harness.CreateProfile().GetRootFolder(), [Led(LedId.Keyboard_A)]); if (fadeToBlue) { SKColorLayerProperty color = brush.Properties.Color; color.KeyframesEnabled = true; color.AddKeyframe(new LayerPropertyKeyframe(SKColors.Red, TimeSpan.Zero, Easings.Functions.Linear, color)); color.AddKeyframe(new LayerPropertyKeyframe(SKColors.Blue, TimeSpan.FromSeconds(1), Easings.Functions.Linear, color)); } EventCondition condition = UseEventCondition(layer, eventPath, triggerMode, overlapMode, toggleOffMode); return (layer, condition); } private EventCondition UseEventCondition(RenderProfileElement element, string eventPath, EventTriggerMode triggerMode = EventTriggerMode.Play, EventOverlapMode overlapMode = EventOverlapMode.Restart, EventToggleOffMode toggleOffMode = EventToggleOffMode.Finish) { element.Timeline.StartSegmentLength = TimeSpan.Zero; element.Timeline.MainSegmentLength = TimeSpan.FromSeconds(1); element.Timeline.EndSegmentLength = TimeSpan.Zero; EventCondition condition = new(element) { EventPath = new DataModelPath(DataModel, eventPath), TriggerMode = triggerMode, OverlapMode = overlapMode, ToggleOffMode = toggleOffMode }; // Like the editor does after picking an event condition.UpdateEventNode(true); element.DisplayCondition = condition; return condition; } private TestGreaterThanNode RequireValueAbove(EventCondition condition, float threshold) { TestGreaterThanNode greaterThan = _nodes.AddNode(condition.Script); greaterThan.Threshold = threshold; condition.GetStartNode().Pins.Single(p => p.Name == "Value").ConnectTo(greaterThan.Input); condition.Script.Nodes.OfType>().Single().Input.ConnectTo(greaterThan.Output); return greaterThan; } private static SKColor ColorAt(double seconds) { return new SKColor((byte) Math.Round(255 * (1 - seconds)), 0, (byte) Math.Round(255 * seconds)); } private void ShouldBeOn(RenderFrame frame) { frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Red); } private void ShouldBeOff(RenderFrame frame) { frame.GetColor(Keyboard, LedId.Keyboard_A).ShouldBeColor(SKColors.Black); } }