From 76a3ea4043e0243c22524d4d16d7a716424b659f Mon Sep 17 00:00:00 2001 From: dvolper Date: Fri, 29 Sep 2023 15:36:38 +0200 Subject: [PATCH] Refactor internal frame tracking & time handling (#29) * rework FrameTracker to consistently trigger on next frame * use new FrameTracker implementation * remove unnecessary difference between server & client side frame tracking * listbox in dialog test * prettier * ci & cd (install .net 6 & 7) * try CI on windows * disable prettier check on windows CI * fix prettier check on windows CI * switch CI back to ubuntu-latest & stop using Task.Delay to wait on state/component changes in tests * use TimeProvider instead of Task.Delay for transitions * use TimeProvider instead of Task.Delay * code format * update tests to use TestTimeProvider * fixes * introduce ITimer interface * code format * TestTimer implementation * fix tests --- .gitattributes | 2 +- .github/workflows/ci.yml | 4 +- .github/workflows/nuget-cd.yml | 4 +- .prettierignore | 1 + packages/Ignis.Components/FrameTracker.cs | 40 +++++---- packages/Ignis.Components/ITimer.cs | 7 ++ .../Ignis.Components/Ignis.Components.csproj | 1 + .../IgnisComponentExtensions.cs | 2 + packages/Ignis.Components/TimeProvider.cs | 11 +++ .../TimeProviderImplementation.cs | 5 ++ .../Ignis.Components/TimerImplementation.cs | 16 ++++ .../Ignis.Components.HeadlessUI/Dialog.cs | 6 +- .../OpenCloseWithTransitionComponentBase.cs | 6 +- .../Ignis.Components.HeadlessUI/Transition.cs | 7 +- .../TransitionBase.cs | 49 +++++++---- .../Ignis.Tests.Common/IgnisTestExtensions.cs | 18 +++++ tests/Ignis.Tests.Common/TestHostContext.cs | 2 +- tests/Ignis.Tests.Common/TestTimeProvider.cs | 11 +++ tests/Ignis.Tests.Common/TestTimer.cs | 30 +++++++ .../DialogTests.razor | 81 +++++++++++-------- .../ListboxInDialog.razor | 40 +++++++++ .../ListboxInDialogTest.razor | 7 ++ .../ListboxTests.razor | 38 ++++++++- .../MenuTests.razor | 6 +- .../PopoverTests.razor | 6 +- .../ReactiveSectionTests.razor | 3 +- .../ReactiveValueTests.razor | 6 +- .../IgnisAsyncComponentBaseTests.razor | 6 +- 28 files changed, 309 insertions(+), 106 deletions(-) create mode 100644 packages/Ignis.Components/ITimer.cs create mode 100644 packages/Ignis.Components/TimeProvider.cs create mode 100644 packages/Ignis.Components/TimeProviderImplementation.cs create mode 100644 packages/Ignis.Components/TimerImplementation.cs create mode 100644 tests/Ignis.Tests.Common/IgnisTestExtensions.cs create mode 100644 tests/Ignis.Tests.Common/TestTimeProvider.cs create mode 100644 tests/Ignis.Tests.Common/TestTimer.cs create mode 100644 tests/Ignis.Tests.Components.HeadlessUI/ListboxInDialog.razor create mode 100644 tests/Ignis.Tests.Components.HeadlessUI/ListboxInDialogTest.razor diff --git a/.gitattributes b/.gitattributes index 21256661..94f480de 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -* text=auto \ No newline at end of file +* text=auto eol=lf \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d31fef50..76bc61b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,9 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-dotnet@v3 with: - dotnet-version: 7.0.x + dotnet-version: | + 6.0.x + 7.0.x - uses: actions/setup-node@v3 with: node-version: 18.x diff --git a/.github/workflows/nuget-cd.yml b/.github/workflows/nuget-cd.yml index 8f654505..3cb16178 100644 --- a/.github/workflows/nuget-cd.yml +++ b/.github/workflows/nuget-cd.yml @@ -19,7 +19,9 @@ jobs: fetch-depth: 0 - uses: actions/setup-dotnet@v3 with: - dotnet-version: 7.0.x + dotnet-version: | + 6.0.x + 7.0.x - uses: actions/setup-node@v3 with: node-version: 18.x diff --git a/.prettierignore b/.prettierignore index 56215ca0..fd8b708b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,3 +8,4 @@ website/Ignis.Website.Server/wwwroot/_docs website/Ignis.Website.WebAssembly/wwwroot/_docs website/Ignis.Website/wwwroot/js/website.min.js website/Ignis.Website/wwwroot/css/tailwind.min.css +tests/e2e/Ignis.Tests.E2E.Website/wwwroot/css/app.min.css diff --git a/packages/Ignis.Components/FrameTracker.cs b/packages/Ignis.Components/FrameTracker.cs index c0e53296..a6e66f9a 100644 --- a/packages/Ignis.Components/FrameTracker.cs +++ b/packages/Ignis.Components/FrameTracker.cs @@ -2,38 +2,36 @@ internal class FrameTracker { - private readonly IHostContext _hostContext; - + private long _currentFrame; + private long? _frameToExecuteOn; + private IgnisComponentBase? _target; private Action? _action; public bool IsPending => _action != null; - public FrameTracker(IHostContext hostContext) - { - _hostContext = hostContext ?? throw new ArgumentNullException(nameof(hostContext)); - } - - public void ExecuteOnNextFrame(Action action, Action update) + public void ExecuteOnNextFrame(IgnisComponentBase target, Action action) { - if (action == null) throw new ArgumentNullException(nameof(action)); - - // If we're server-side, we can just execute the action on the next render, otherwise we need to wait for the second render. (WebAssembly) - _action = _hostContext.IsServerSide - ? action - : () => - { - _action = action; + _target = target ?? throw new ArgumentNullException(nameof(target)); + _action = action ?? throw new ArgumentNullException(nameof(action)); - update(obj: false); - }; + _frameToExecuteOn = _currentFrame + 1; } public void OnAfterRender() { - var action = _action; + if (_currentFrame >= _frameToExecuteOn) + { + _frameToExecuteOn = null; + + _action?.Invoke(); - _action = null; + _action = null; + } + else if (_frameToExecuteOn.HasValue) + { + _target?.Update(); + } - action?.Invoke(); + ++_currentFrame; } } diff --git a/packages/Ignis.Components/ITimer.cs b/packages/Ignis.Components/ITimer.cs new file mode 100644 index 00000000..d1c40465 --- /dev/null +++ b/packages/Ignis.Components/ITimer.cs @@ -0,0 +1,7 @@ +namespace Ignis.Components; + +//TODO switch to System.Threading.ITimer when it's available +// https://learn.microsoft.com/en-us/dotnet/api/system.threading.itimer?view=net-8.0 +internal interface ITimer : IDisposable +{ +} diff --git a/packages/Ignis.Components/Ignis.Components.csproj b/packages/Ignis.Components/Ignis.Components.csproj index 264c780e..d21ac4a0 100644 --- a/packages/Ignis.Components/Ignis.Components.csproj +++ b/packages/Ignis.Components/Ignis.Components.csproj @@ -23,6 +23,7 @@ + diff --git a/packages/Ignis.Components/IgnisComponentExtensions.cs b/packages/Ignis.Components/IgnisComponentExtensions.cs index cd2d0719..15e32f80 100644 --- a/packages/Ignis.Components/IgnisComponentExtensions.cs +++ b/packages/Ignis.Components/IgnisComponentExtensions.cs @@ -15,6 +15,8 @@ public static IServiceCollection AddIgnis(this IServiceCollection serviceCollect serviceCollection.TryAddScoped(); + serviceCollection.TryAddSingleton(); + return serviceCollection; } diff --git a/packages/Ignis.Components/TimeProvider.cs b/packages/Ignis.Components/TimeProvider.cs new file mode 100644 index 00000000..6d807379 --- /dev/null +++ b/packages/Ignis.Components/TimeProvider.cs @@ -0,0 +1,11 @@ +namespace Ignis.Components; + +//TODO switch to System.TimeProvider when it's available +// https://learn.microsoft.com/en-us/dotnet/api/system.timeprovider?view=net-8.0 +internal abstract class TimeProvider +{ + public virtual ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) + { + return new TimerImplementation(callback, state, dueTime, period); + } +} diff --git a/packages/Ignis.Components/TimeProviderImplementation.cs b/packages/Ignis.Components/TimeProviderImplementation.cs new file mode 100644 index 00000000..56cdc478 --- /dev/null +++ b/packages/Ignis.Components/TimeProviderImplementation.cs @@ -0,0 +1,5 @@ +namespace Ignis.Components; + +internal class TimeProviderImplementation : TimeProvider +{ +} diff --git a/packages/Ignis.Components/TimerImplementation.cs b/packages/Ignis.Components/TimerImplementation.cs new file mode 100644 index 00000000..e19fc3ae --- /dev/null +++ b/packages/Ignis.Components/TimerImplementation.cs @@ -0,0 +1,16 @@ +namespace Ignis.Components; + +internal class TimerImplementation : ITimer +{ + private readonly Timer _timer; + + public TimerImplementation(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) + { + _timer = new Timer(callback, state, dueTime, period); + } + + public void Dispose() + { + _timer.Dispose(); + } +} diff --git a/packages/Tailwind/Ignis.Components.HeadlessUI/Dialog.cs b/packages/Tailwind/Ignis.Components.HeadlessUI/Dialog.cs index 9205cef3..175026df 100644 --- a/packages/Tailwind/Ignis.Components.HeadlessUI/Dialog.cs +++ b/packages/Tailwind/Ignis.Components.HeadlessUI/Dialog.cs @@ -179,7 +179,7 @@ public void Open(Action? continueWith = null) var __ = IsOpenChanged.InvokeAsync(_isOpen = true); - if (continueWith != null) FrameTracker.ExecuteOnNextFrame(continueWith, Update); + if (continueWith != null) FrameTracker.ExecuteOnNextFrame(this, continueWith); Update(); } @@ -206,7 +206,7 @@ private void CloseCore(Action? continueWith, bool async = false) { var __ = IsOpenChanged.InvokeAsync(_isOpen = false); - if (continueWith != null) FrameTracker.ExecuteOnNextFrame(continueWith, Update); + if (continueWith != null) FrameTracker.ExecuteOnNextFrame(this, continueWith); Update(async); } @@ -226,7 +226,7 @@ public void SetDescription(IDialogDescription description) /// public void CloseFromTransition(Action? continueWith = null) { - CloseCore(continueWith, true); + CloseCore(continueWith, async: true); } /// diff --git a/packages/Tailwind/Ignis.Components.HeadlessUI/OpenCloseWithTransitionComponentBase.cs b/packages/Tailwind/Ignis.Components.HeadlessUI/OpenCloseWithTransitionComponentBase.cs index 2cdcdbc9..911949ae 100644 --- a/packages/Tailwind/Ignis.Components.HeadlessUI/OpenCloseWithTransitionComponentBase.cs +++ b/packages/Tailwind/Ignis.Components.HeadlessUI/OpenCloseWithTransitionComponentBase.cs @@ -38,8 +38,8 @@ public void Open(Action? continueWith = null) _ = IsOpenChanged.InvokeAsync(_isOpen = true); if (_transition != null) - FrameTracker.ExecuteOnNextFrame(() => _transition.Show(() => OnAfterOpen(continueWith)), Update); - else if (continueWith != null) FrameTracker.ExecuteOnNextFrame(() => OnAfterOpen(continueWith), Update); + FrameTracker.ExecuteOnNextFrame(this, () => _transition.Show(() => OnAfterOpen(continueWith))); + else if (continueWith != null) FrameTracker.ExecuteOnNextFrame(this, () => OnAfterOpen(continueWith)); Update(); } @@ -73,7 +73,7 @@ private void CloseCore(Action? continueWith, bool async = false) { _ = IsOpenChanged.InvokeAsync(_isOpen = false); - if (continueWith != null) FrameTracker.ExecuteOnNextFrame(continueWith, Update); + if (continueWith != null) FrameTracker.ExecuteOnNextFrame(this, continueWith); Update(async); } diff --git a/packages/Tailwind/Ignis.Components.HeadlessUI/Transition.cs b/packages/Tailwind/Ignis.Components.HeadlessUI/Transition.cs index b91872e9..6153e2de 100644 --- a/packages/Tailwind/Ignis.Components.HeadlessUI/Transition.cs +++ b/packages/Tailwind/Ignis.Components.HeadlessUI/Transition.cs @@ -58,7 +58,6 @@ public bool Show [Parameter] public bool Appear { get; set; } - /// [CascadingParameter] public IContentHost? Outlet { get; set; } [CascadingParameter] public IMenu? Menu { get; set; } @@ -174,7 +173,7 @@ protected override void LeaveTransition(Action? continueWith = null) { _transitioningTo = false; - WatchTransition(false, () => + WatchTransition(isEnter: false, () => { _show = false; @@ -221,7 +220,7 @@ private void WatchTransition(bool isEnter, Action? continueWith) var startedTransitions = new List(); var finishedTransitions = 0; - if (isEnter) base.EnterTransition(() => AggregateDialogs(true, ContinueWith)); + if (isEnter) base.EnterTransition(() => AggregateDialogs(open: true, ContinueWith)); else ContinueWith(); return; @@ -243,7 +242,7 @@ void ContinueWith() if (finishedTransitions == startedTransitions.Count + 1) { if (isEnter) continueWith?.Invoke(); - else AggregateDialogs(false, () => base.LeaveTransition(continueWith)); + else AggregateDialogs(open: false, () => base.LeaveTransition(continueWith)); } } } diff --git a/packages/Tailwind/Ignis.Components.HeadlessUI/TransitionBase.cs b/packages/Tailwind/Ignis.Components.HeadlessUI/TransitionBase.cs index 1a3b0995..ffab3c98 100644 --- a/packages/Tailwind/Ignis.Components.HeadlessUI/TransitionBase.cs +++ b/packages/Tailwind/Ignis.Components.HeadlessUI/TransitionBase.cs @@ -33,7 +33,8 @@ public abstract class TransitionBase : IgnisComponentBase, ICssClass, IHandleAft { get { - var originalClassString = AdditionalAttributes?.FirstOrDefault(a => string.Equals(a.Key, "class", StringComparison.OrdinalIgnoreCase)); + var originalClassString = AdditionalAttributes?.FirstOrDefault(a => + string.Equals(a.Key, "class", StringComparison.OrdinalIgnoreCase)); return _state switch { TransitionState.Entering => $"{originalClassString?.Value} {Enter} {EnterFrom}".Trim(), @@ -68,6 +69,8 @@ public abstract class TransitionBase : IgnisComponentBase, ICssClass, IHandleAft [Inject] internal FrameTracker FrameTracker { get; set; } = null!; + [Inject] internal TimeProvider TimeProvider { get; set; } = null!; + protected virtual void EnterTransition(Action? continueWith = null) { if (_state != TransitionState.Default && _state != TransitionState.CanEnter) return; @@ -76,17 +79,22 @@ protected virtual void EnterTransition(Action? continueWith = null) UpdateState(TransitionState.Entering, () => { + ITimer timer = null!; var (graceDuration, transitionDuration) = ParseDuration(Enter); - _ = Task.Delay(graceDuration).ContinueWith(__ => + timer = TimeProvider.CreateTimer(_ => { + // ReSharper disable once AccessToModifiedClosure + timer.Dispose(); UpdateState(TransitionState.Entered, () => { - _ = Task.Delay(transitionDuration).ContinueWith(_ => + timer = TimeProvider.CreateTimer(_ => { + // ReSharper disable once AccessToModifiedClosure + timer.Dispose(); UpdateState(TransitionState.CanLeave, continueWith); - }); + }, state: null, transitionDuration, Timeout.InfiniteTimeSpan); }); - }); + }, state: null, graceDuration, Timeout.InfiniteTimeSpan); }); } @@ -98,19 +106,25 @@ protected virtual void LeaveTransition(Action? continueWith = null) UpdateState(TransitionState.Leaving, () => { + ITimer timer = null!; var (graceDuration, transitionDuration) = ParseDuration(Leave); - _ = Task.Delay(graceDuration).ContinueWith(__ => + timer = TimeProvider.CreateTimer(_ => { + // ReSharper disable once AccessToModifiedClosure + timer.Dispose(); UpdateState(TransitionState.Left, () => { - _ = Task.Delay(transitionDuration).ContinueWith(_ => + timer = TimeProvider.CreateTimer(_ => { + // ReSharper disable once AccessToModifiedClosure + timer.Dispose(); + RenderContent = false; UpdateState(TransitionState.CanEnter, continueWith); - }); + }, state: null, transitionDuration, Timeout.InfiniteTimeSpan); }); - }); + }, state: null, graceDuration, Timeout.InfiniteTimeSpan); }); } @@ -120,9 +134,9 @@ private void UpdateState(TransitionState state, Action? continueWith) _state = state; - if (continueWith != null) FrameTracker.ExecuteOnNextFrame(continueWith, Update); + if (continueWith != null) FrameTracker.ExecuteOnNextFrame(this, continueWith); - Update(true); + Update(async: true); } /// @@ -133,12 +147,12 @@ public virtual Task OnAfterRenderAsync() return Task.CompletedTask; } - private static (int, int) ParseDuration(string? classString) + private static (TimeSpan, TimeSpan) ParseDuration(string? classString) { var durationClass = classString?.Split(' ') .Select(v => v.Trim().Split(':')[v.Trim().Split(':').Length - 1]) .FirstOrDefault(v => v.StartsWith("duration-", StringComparison.Ordinal)); - if (durationClass == null) return (0, 0); + if (durationClass == null) return (TimeSpan.Zero, TimeSpan.Zero); var factor = 1; @@ -156,15 +170,16 @@ private static (int, int) ParseDuration(string? classString) durationString = durationString[..^1]; factor = 1000; } - else return (0, 0); + else return (TimeSpan.Zero, TimeSpan.Zero); } var duration = int.Parse(durationString, CultureInfo.InvariantCulture) * factor; - if (duration <= 0) return (0, 0); + if (duration <= 0) return (TimeSpan.Zero, TimeSpan.Zero); return duration < TransitionGraceDuration - ? (0, duration) - : (TransitionGraceDuration, duration - TransitionGraceDuration); + ? (TimeSpan.Zero, TimeSpan.FromMilliseconds(duration)) + : (TimeSpan.FromMilliseconds(TransitionGraceDuration), + TimeSpan.FromMilliseconds(duration - TransitionGraceDuration)); } private enum TransitionState diff --git a/tests/Ignis.Tests.Common/IgnisTestExtensions.cs b/tests/Ignis.Tests.Common/IgnisTestExtensions.cs new file mode 100644 index 00000000..fa38cf09 --- /dev/null +++ b/tests/Ignis.Tests.Common/IgnisTestExtensions.cs @@ -0,0 +1,18 @@ +using Ignis.Components; +using Microsoft.Extensions.DependencyInjection; + +namespace Ignis.Tests.Common; + +public static class IgnisTestExtensions +{ + public static IServiceCollection AddIgnisTestServices(this IServiceCollection serviceCollection) + { + if (serviceCollection == null) throw new ArgumentNullException(nameof(serviceCollection)); + + serviceCollection.AddIgnis(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + + return serviceCollection; + } +} diff --git a/tests/Ignis.Tests.Common/TestHostContext.cs b/tests/Ignis.Tests.Common/TestHostContext.cs index cc74fda0..94b5dcd4 100644 --- a/tests/Ignis.Tests.Common/TestHostContext.cs +++ b/tests/Ignis.Tests.Common/TestHostContext.cs @@ -2,7 +2,7 @@ namespace Ignis.Tests.Common; -public class TestHostContext : IHostContext +internal class TestHostContext : IHostContext { public bool IsPrerendering => false; diff --git a/tests/Ignis.Tests.Common/TestTimeProvider.cs b/tests/Ignis.Tests.Common/TestTimeProvider.cs new file mode 100644 index 00000000..02f279e4 --- /dev/null +++ b/tests/Ignis.Tests.Common/TestTimeProvider.cs @@ -0,0 +1,11 @@ +using Ignis.Components; + +namespace Ignis.Tests.Common; + +internal class TestTimeProvider : TimeProvider +{ + public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) + { + return new TestTimer(callback); + } +} diff --git a/tests/Ignis.Tests.Common/TestTimer.cs b/tests/Ignis.Tests.Common/TestTimer.cs new file mode 100644 index 00000000..3766231c --- /dev/null +++ b/tests/Ignis.Tests.Common/TestTimer.cs @@ -0,0 +1,30 @@ +using Ignis.Components; + +namespace Ignis.Tests.Common; + +public sealed class TestTimer : ITimer +{ + private static readonly List Timers = new(); + + private readonly TimerCallback _callback; + + public TestTimer(TimerCallback callback) + { + _callback = callback; + + Timers.Add(this); + } + + public static void Trigger(object? state) + { + foreach (var timer in Timers.ToArray()) + { + timer._callback(state); + } + } + + public void Dispose() + { + Timers.Remove(this); + } +} diff --git a/tests/Ignis.Tests.Components.HeadlessUI/DialogTests.razor b/tests/Ignis.Tests.Components.HeadlessUI/DialogTests.razor index 557750f7..6592a0e0 100644 --- a/tests/Ignis.Tests.Components.HeadlessUI/DialogTests.razor +++ b/tests/Ignis.Tests.Components.HeadlessUI/DialogTests.razor @@ -5,8 +5,7 @@ [Fact] public void Outlet() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); JSInterop.Mode = JSRuntimeMode.Loose; @@ -29,8 +28,7 @@ [Fact] public void IgnoreOutlet() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); JSInterop.Mode = JSRuntimeMode.Loose; @@ -53,8 +51,7 @@ [Fact] public void OutletWithTransition() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); JSInterop.Mode = JSRuntimeMode.Loose; @@ -85,8 +82,7 @@ [Fact] public void IgnoreOutletWithTransition() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); JSInterop.Mode = JSRuntimeMode.Loose; @@ -117,8 +113,7 @@ [Fact] public async Task OutletWithTransitionAndChildren() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); JSInterop.Mode = JSRuntimeMode.Loose; @@ -142,12 +137,17 @@ var transition = cut.FindComponent(); transition.SetParametersAndRender(p => { p.Add(x => x.Show, true); }); - await Task.Delay(100); + // Transition + TestTimer.Trigger(null); + TestTimer.Trigger(null); + // TransitionChild + TestTimer.Trigger(null); + TestTimer.Trigger(null); var transitions = cut.FindAll($"#{transitionId}"); Assert.Single(transitions); - var dialogs = cut.FindAll($"#{dialogId}"); + var dialogs = cut.WaitForElements($"#{dialogId}"); Assert.Single(dialogs); var dialogDiv = dialogs.Single(); @@ -164,8 +164,7 @@ [Fact] public async Task OutletWithTransitionAndChildren_EnterLeaveWithDuration() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); JSInterop.Mode = JSRuntimeMode.Loose; @@ -189,9 +188,14 @@ var transition = cut.FindComponent(); transition.SetParametersAndRender(p => { p.Add(x => x.Show, true); }); - await Task.Delay(400); + // Transition + TestTimer.Trigger(null); + TestTimer.Trigger(null); + // TransitionChild + TestTimer.Trigger(null); + TestTimer.Trigger(null); - var transitions = cut.FindAll($"#{transitionId}"); + var transitions = cut.WaitForElements($"#{transitionId}"); Assert.Single(transitions); var dialogs = cut.FindAll($"#{dialogId}"); @@ -209,14 +213,18 @@ transition.SetParametersAndRender(p => { p.Add(x => x.Show, false); }); - await Task.Delay(300); + // Transition + TestTimer.Trigger(null); + TestTimer.Trigger(null); + // TransitionChild + TestTimer.Trigger(null); + TestTimer.Trigger(null); + cut.WaitForState(() => cut.FindAll($"#{dialogId}").Count == 0); + transitions = cut.FindAll($"#{transitionId}"); Assert.Single(transitions); - - dialogs = cut.FindAll($"#{dialogId}"); - Assert.Empty(dialogs); - + transitionDiv = transitions.Single(); outletDiv = cut.Find($"#{outletId}"); var transitionChildDivs = cut.FindAll($"#{transitionChildId}"); @@ -229,8 +237,7 @@ [Fact] public async Task CustomModal_OpenCloseByButton() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); JSInterop.Mode = JSRuntimeMode.Loose; @@ -238,7 +245,7 @@ const string openButtonId = "open-button"; const string outletId = "dialog-outlet"; - var cut = Render(@); + var cut = Render(@); var outlet = cut.Find($"#{outletId}"); Assert.Empty(outlet.Children); @@ -246,17 +253,27 @@ var openButton = cut.Find($"#{openButtonId}"); openButton.Click(); - await Task.Delay(400); - cut.Render(); - - var closeButton = cut.Find($"#{closeButtonId}"); + // Transition + TestTimer.Trigger(null); + TestTimer.Trigger(null); + // TransitionChild + TestTimer.Trigger(null); + TestTimer.Trigger(null); + + var closeButton = cut.WaitForElement($"#{closeButtonId}"); Assert.True(outlet.Contains(closeButton)); - + closeButton.Click(); - await Task.Delay(300); - cut.Render(); - + // Transition + TestTimer.Trigger(null); + TestTimer.Trigger(null); + // TransitionChild + TestTimer.Trigger(null); + TestTimer.Trigger(null); + + cut.WaitForState(() => cut.FindAll($"#{closeButtonId}").Count == 0); + outlet = cut.Find($"#{outletId}"); Assert.Empty(outlet.Children); } diff --git a/tests/Ignis.Tests.Components.HeadlessUI/ListboxInDialog.razor b/tests/Ignis.Tests.Components.HeadlessUI/ListboxInDialog.razor new file mode 100644 index 00000000..6c457aa6 --- /dev/null +++ b/tests/Ignis.Tests.Components.HeadlessUI/ListboxInDialog.razor @@ -0,0 +1,40 @@ +@inherits IgnisRigidComponentBase + + + + + + + + @foreach (var option in _options) + { + + @option.Name + + } + + + + + +@code +{ + private readonly Type[] _options = + { + typeof(string), + typeof(int), + typeof(bool) + }; + + private Type? _selectedType; + + [Parameter] + public bool Open { get; set; } + + [Parameter] + public EventCallback OpenChanged { get; set; } +} \ No newline at end of file diff --git a/tests/Ignis.Tests.Components.HeadlessUI/ListboxInDialogTest.razor b/tests/Ignis.Tests.Components.HeadlessUI/ListboxInDialogTest.razor new file mode 100644 index 00000000..064c5258 --- /dev/null +++ b/tests/Ignis.Tests.Components.HeadlessUI/ListboxInDialogTest.razor @@ -0,0 +1,7 @@ + + + +@code +{ + bool _isOpen; +} \ No newline at end of file diff --git a/tests/Ignis.Tests.Components.HeadlessUI/ListboxTests.razor b/tests/Ignis.Tests.Components.HeadlessUI/ListboxTests.razor index f01f5aa1..7a025f9f 100644 --- a/tests/Ignis.Tests.Components.HeadlessUI/ListboxTests.razor +++ b/tests/Ignis.Tests.Components.HeadlessUI/ListboxTests.razor @@ -5,8 +5,7 @@ [Fact] public void Button_OnClick() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); JSInterop.Mode = JSRuntimeMode.Loose; @@ -28,8 +27,7 @@ [Fact] public void Button_OnClick_PreventDefault() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); JSInterop.Mode = JSRuntimeMode.Loose; @@ -43,4 +41,36 @@ var listbox = cut.FindComponent>(); Assert.False(listbox.Instance.IsOpen); } + + [Fact] + public async Task ListboxInDialog() + { + Services.AddIgnisTestServices(); + + JSInterop.Mode = JSRuntimeMode.Loose; + + const string openButtonId = "open-button"; + const string listboxButtonId = "listbox-button"; + const string intOptionId = nameof(Int32); + + var cut = Render(@); + + var openButton = cut.Find($"#{openButtonId}"); + openButton.Click(); + + TestTimer.Trigger(null); + TestTimer.Trigger(null); + + var listboxButton = cut.WaitForElement($"#{listboxButtonId}"); + listboxButton.Click(); + + var intOption = cut.WaitForElement($"#{intOptionId}"); + intOption.Click(); + + cut.WaitForAssertion(() => + { + var listbox = cut.FindComponent>(); + Assert.Equal(typeof(int), listbox.Instance.Value); + }); + } } \ No newline at end of file diff --git a/tests/Ignis.Tests.Components.HeadlessUI/MenuTests.razor b/tests/Ignis.Tests.Components.HeadlessUI/MenuTests.razor index 14a49030..2f047d5c 100644 --- a/tests/Ignis.Tests.Components.HeadlessUI/MenuTests.razor +++ b/tests/Ignis.Tests.Components.HeadlessUI/MenuTests.razor @@ -5,8 +5,7 @@ [Fact] public void Button_OnClick() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); JSInterop.Mode = JSRuntimeMode.Loose; @@ -28,8 +27,7 @@ [Fact] public void Button_OnClick_PreventDefault() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); JSInterop.Mode = JSRuntimeMode.Loose; diff --git a/tests/Ignis.Tests.Components.HeadlessUI/PopoverTests.razor b/tests/Ignis.Tests.Components.HeadlessUI/PopoverTests.razor index 1bec9ade..3ea6f7ed 100644 --- a/tests/Ignis.Tests.Components.HeadlessUI/PopoverTests.razor +++ b/tests/Ignis.Tests.Components.HeadlessUI/PopoverTests.razor @@ -5,8 +5,7 @@ [Fact] public void Button_OnClick() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); JSInterop.Mode = JSRuntimeMode.Loose; @@ -28,8 +27,7 @@ [Fact] public void Button_OnClick_PreventDefault() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); JSInterop.Mode = JSRuntimeMode.Loose; diff --git a/tests/Ignis.Tests.Components.Reactivity/ReactiveSectionTests.razor b/tests/Ignis.Tests.Components.Reactivity/ReactiveSectionTests.razor index 0a191dad..76ab090f 100644 --- a/tests/Ignis.Tests.Components.Reactivity/ReactiveSectionTests.razor +++ b/tests/Ignis.Tests.Components.Reactivity/ReactiveSectionTests.razor @@ -5,8 +5,7 @@ [Fact] public void Test() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); var cut = RenderComponent(); diff --git a/tests/Ignis.Tests.Components.Reactivity/ReactiveValueTests.razor b/tests/Ignis.Tests.Components.Reactivity/ReactiveValueTests.razor index e7f08059..477463f0 100644 --- a/tests/Ignis.Tests.Components.Reactivity/ReactiveValueTests.razor +++ b/tests/Ignis.Tests.Components.Reactivity/ReactiveValueTests.razor @@ -5,8 +5,7 @@ [Fact] public void SilentCounter() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); var cut = RenderComponent(); Assert.Equal(1, cut.RenderCount); @@ -25,8 +24,7 @@ [Fact] public void ReactiveCounter() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); var cut = RenderComponent(); Assert.Equal(1, cut.RenderCount); diff --git a/tests/Ignis.Tests.Components/IgnisAsyncComponentBaseTests.razor b/tests/Ignis.Tests.Components/IgnisAsyncComponentBaseTests.razor index 69801fb2..291af82f 100644 --- a/tests/Ignis.Tests.Components/IgnisAsyncComponentBaseTests.razor +++ b/tests/Ignis.Tests.Components/IgnisAsyncComponentBaseTests.razor @@ -5,8 +5,7 @@ [Fact] public void Cycle() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); const string echo1 = "Hello World!"; const string echo2 = "Lorem Ipsum dolor sit amet."; @@ -27,8 +26,7 @@ [Fact] public void AlreadyDisposed() { - Services.AddIgnis(); - Services.AddSingleton(); + Services.AddIgnisTestServices(); const string echo1 = "Hello World!"; const string echo2 = "Lorem Ipsum dolor sit amet.";