using Microsoft.Playwright; namespace Enciphered.Blazor.UIComponents.Tests; /// /// Tests that cover interactive behavior gaps to ensure safe JS migration. /// Covers: NumberInput +/- buttons & min/max clamping, Popover open/close mechanics, /// Calendar arrow navigation, and trigger text updates for Date/Time/DateTime inputs. /// [TestFixture] public class InteractivityTests : PlaywrightTestBase { // ── Helpers ────────────────────────────────────────────────────────────── private async Task GoToFormsAsync() { await Page.GotoAsync($"{BaseUrl}/forms", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); await Page.WaitForSelectorAsync("[data-testid='btn-submit']", new PageWaitForSelectorOptions { Timeout = 10_000 }); } private ILocator Input(string testId) => Page.Locator($"[data-testid='{testId}']"); private ILocator Trigger(string testId) => Page.Locator($"[data-testid='{testId}']"); /// /// Get the popover panel scoped to the popover containing a trigger. /// private ILocator PopoverPanelFor(string triggerId) => Trigger(triggerId).Locator("xpath=ancestor::*[@data-popover]").Locator("[data-popover-panel]"); private ILocator PopoverBackdropFor(string triggerId) => Trigger(triggerId).Locator("xpath=ancestor::*[@data-popover]").Locator("[data-popover-backdrop]"); /// /// Navigate the open calendar to a specific month/year using the month and year pickers. /// private async Task NavigateCalendarToDate(ILocator panel, DateOnly target) { var yearButton = panel.Locator("[data-calendar-year]"); await yearButton.ClickAsync(); await Page.WaitForTimeoutAsync(150); var yearGrid = panel.Locator(".grid.grid-cols-4"); var targetYearBtn = yearGrid.Locator($"button:has-text('{target.Year}')"); var attempts = 0; while (await targetYearBtn.CountAsync() == 0 && attempts < 10) { var firstYearText = await yearGrid.Locator("button").First.InnerTextAsync(); var firstYear = int.Parse(firstYearText.Trim()); if (target.Year < firstYear) await panel.Locator("button[aria-label='Previous month']").ClickAsync(); else await panel.Locator("button[aria-label='Next month']").ClickAsync(); await Page.WaitForTimeoutAsync(100); yearGrid = panel.Locator(".grid.grid-cols-4"); targetYearBtn = yearGrid.Locator($"button:has-text('{target.Year}')"); attempts++; } await targetYearBtn.First.ClickAsync(); await Page.WaitForTimeoutAsync(150); var monthButton = panel.Locator("[data-calendar-month]"); await monthButton.ClickAsync(); await Page.WaitForTimeoutAsync(150); var monthGrid = panel.Locator(".grid.grid-cols-3"); var targetMonthText = new DateOnly(2000, target.Month, 1).ToString("MMM"); await monthGrid.Locator($"button:has-text('{targetMonthText}')").First.ClickAsync(); await Page.WaitForTimeoutAsync(150); } /// /// Pick hour, minute, and AM/PM in an already-open time picker popover. /// private async Task PickTimeInOpenPopover(ILocator panel, int hour, int minute) { var isPm = hour >= 12; var hour12 = hour % 12; if (hour12 == 0) hour12 = 12; var hourText = hour12.ToString("D2"); var hourColumn = panel.Locator(".scrollbar-thin").First; await hourColumn.Locator($"button:has-text('{hourText}')").First.ClickAsync(); await Page.WaitForTimeoutAsync(50); var minuteText = minute.ToString("D2"); var minuteColumn = panel.Locator(".scrollbar-thin").Nth(1); await minuteColumn.Locator($"button:has-text('{minuteText}')").First.ClickAsync(); await Page.WaitForTimeoutAsync(50); var periodText = isPm ? "PM" : "AM"; await panel.Locator($"button:has-text('{periodText}')").First.ClickAsync(); await Page.WaitForTimeoutAsync(50); } // ════════════════════════════════════════════════════════════════════════ // NumberInput: Increment / Decrement buttons // ════════════════════════════════════════════════════════════════════════ [Test] public async Task NumberInput_Increment_Button_Increases_Value() { await GoToFormsAsync(); var input = Input("input-age"); await input.FillAsync("25"); await Page.WaitForTimeoutAsync(100); var incrementBtn = Page.Locator("[data-testid='input-age']").Locator("..").Locator("button[aria-label='Increment']"); await incrementBtn.ClickAsync(); await Page.WaitForTimeoutAsync(100); await Expect(input).ToHaveValueAsync("26"); } [Test] public async Task NumberInput_Decrement_Button_Decreases_Value() { await GoToFormsAsync(); var input = Input("input-age"); await input.FillAsync("25"); await Page.WaitForTimeoutAsync(100); var decrementBtn = Page.Locator("[data-testid='input-age']").Locator("..").Locator("button[aria-label='Decrement']"); await decrementBtn.ClickAsync(); await Page.WaitForTimeoutAsync(100); await Expect(input).ToHaveValueAsync("24"); } [Test] public async Task NumberInput_Increment_Multiple_Times() { await GoToFormsAsync(); var input = Input("input-age"); await input.FillAsync("10"); await Page.WaitForTimeoutAsync(100); var incrementBtn = Page.Locator("[data-testid='input-age']").Locator("..").Locator("button[aria-label='Increment']"); await incrementBtn.ClickAsync(); await Page.WaitForTimeoutAsync(50); await incrementBtn.ClickAsync(); await Page.WaitForTimeoutAsync(50); await incrementBtn.ClickAsync(); await Page.WaitForTimeoutAsync(100); await Expect(input).ToHaveValueAsync("13"); } [Test] public async Task NumberInput_Increment_From_Empty_Sets_Value_To_One() { await GoToFormsAsync(); var input = Input("input-age"); await input.FillAsync(""); await Page.WaitForTimeoutAsync(100); var incrementBtn = Page.Locator("[data-testid='input-age']").Locator("..").Locator("button[aria-label='Increment']"); await incrementBtn.ClickAsync(); await Page.WaitForTimeoutAsync(100); await Expect(input).ToHaveValueAsync("1"); } [Test] public async Task NumberInput_Decrement_From_Empty_Sets_Value_To_Negative_One_Or_Min() { await GoToFormsAsync(); var input = Input("input-age"); await input.FillAsync(""); await Page.WaitForTimeoutAsync(100); var decrementBtn = Page.Locator("[data-testid='input-age']").Locator("..").Locator("button[aria-label='Decrement']"); await decrementBtn.ClickAsync(); await Page.WaitForTimeoutAsync(100); // Age has Min=0, so decrement from 0 (default) should clamp to 0 await Expect(input).ToHaveValueAsync("0"); } // ════════════════════════════════════════════════════════════════════════ // NumberInput: Min / Max clamping // ════════════════════════════════════════════════════════════════════════ [Test] public async Task NumberInput_Increment_Button_Disabled_At_Max() { await GoToFormsAsync(); var input = Input("input-age"); await input.FillAsync("150"); // Max is 150 await Page.WaitForTimeoutAsync(100); var incrementBtn = Page.Locator("[data-testid='input-age']").Locator("..").Locator("button[aria-label='Increment']"); await Expect(incrementBtn).ToBeDisabledAsync(); } [Test] public async Task NumberInput_Decrement_Button_Disabled_At_Min() { await GoToFormsAsync(); var input = Input("input-age"); await input.FillAsync("0"); // Min is 0 await Page.WaitForTimeoutAsync(100); var decrementBtn = Page.Locator("[data-testid='input-age']").Locator("..").Locator("button[aria-label='Decrement']"); await Expect(decrementBtn).ToBeDisabledAsync(); } [Test] public async Task NumberInput_Increment_At_Max_Does_Not_Exceed() { await GoToFormsAsync(); var input = Input("input-age"); await input.FillAsync("149"); await Page.WaitForTimeoutAsync(100); var incrementBtn = Page.Locator("[data-testid='input-age']").Locator("..").Locator("button[aria-label='Increment']"); await incrementBtn.ClickAsync(); await Page.WaitForTimeoutAsync(100); await Expect(input).ToHaveValueAsync("150"); await Expect(incrementBtn).ToBeDisabledAsync(); } [Test] public async Task NumberInput_Decrement_At_Min_Does_Not_Go_Below() { await GoToFormsAsync(); var input = Input("input-age"); await input.FillAsync("1"); await Page.WaitForTimeoutAsync(100); var decrementBtn = Page.Locator("[data-testid='input-age']").Locator("..").Locator("button[aria-label='Decrement']"); await decrementBtn.ClickAsync(); await Page.WaitForTimeoutAsync(100); await Expect(input).ToHaveValueAsync("0"); await Expect(decrementBtn).ToBeDisabledAsync(); } // ════════════════════════════════════════════════════════════════════════ // Popover: explicit open/close mechanics // ════════════════════════════════════════════════════════════════════════ [Test] public async Task Popover_Opens_On_Trigger_Click() { await GoToFormsAsync(); // Date input trigger opens a calendar popover await Trigger("trigger-birthdate").ClickAsync(); await Page.WaitForTimeoutAsync(300); // The popover panel scoped to this trigger should be visible var panel = PopoverPanelFor("trigger-birthdate"); await Expect(panel).ToBeVisibleAsync(); } [Test] public async Task Popover_Closes_On_Backdrop_Click() { await GoToFormsAsync(); // Open the time input popover await Trigger("trigger-preferredtime").ClickAsync(); await Page.WaitForTimeoutAsync(300); var panel = PopoverPanelFor("trigger-preferredtime"); await Expect(panel).ToBeVisibleAsync(); // Click the backdrop overlay to close var backdrop = PopoverBackdropFor("trigger-preferredtime"); await backdrop.ClickAsync(new LocatorClickOptions { Force = true }); await Page.WaitForTimeoutAsync(300); // Popover should no longer be visible await Expect(panel).ToBeHiddenAsync(); } [Test] public async Task Popover_Stays_Open_On_Content_Click() { await GoToFormsAsync(); // Open the date input popover await Trigger("trigger-birthdate").ClickAsync(); await Page.WaitForTimeoutAsync(300); var panel = PopoverPanelFor("trigger-birthdate"); await Expect(panel).ToBeVisibleAsync(); // Click inside the popover content (e.g. the month header button) — should NOT close var monthButton = panel.Locator("[data-calendar-month]"); await monthButton.ClickAsync(); await Page.WaitForTimeoutAsync(300); // Popover should still be visible (month picker is now showing) await Expect(panel).ToBeVisibleAsync(); } [Test] public async Task Popover_Toggle_Opens_Then_Closes_Via_Backdrop() { await GoToFormsAsync(); var trigger = Trigger("trigger-birthdate"); var panel = PopoverPanelFor("trigger-birthdate"); // Open await trigger.ClickAsync(); await Page.WaitForTimeoutAsync(300); await Expect(panel).ToBeVisibleAsync(); // Close via backdrop var backdrop = PopoverBackdropFor("trigger-birthdate"); await backdrop.ClickAsync(new LocatorClickOptions { Force = true }); await Page.WaitForTimeoutAsync(300); await Expect(panel).ToBeHiddenAsync(); } // ════════════════════════════════════════════════════════════════════════ // Calendar: Previous / Next arrow buttons // ════════════════════════════════════════════════════════════════════════ [Test] public async Task Calendar_Next_Arrow_Advances_Month() { await GoToFormsAsync(); // Open calendar await Trigger("trigger-birthdate").ClickAsync(); await Page.WaitForTimeoutAsync(300); var panel = PopoverPanelFor("trigger-birthdate"); // Read the current displayed month var monthLabel = panel.Locator("[data-calendar-month]"); var initialMonth = await monthLabel.InnerTextAsync(); // Click the next arrow await panel.Locator("button[aria-label='Next month']").ClickAsync(); await Page.WaitForTimeoutAsync(200); // Month should have changed var newMonth = await monthLabel.InnerTextAsync(); Assert.That(newMonth, Is.Not.EqualTo(initialMonth), "Month label should change after clicking Next"); } [Test] public async Task Calendar_Previous_Arrow_Goes_Back_Month() { await GoToFormsAsync(); // Open calendar await Trigger("trigger-birthdate").ClickAsync(); await Page.WaitForTimeoutAsync(300); var panel = PopoverPanelFor("trigger-birthdate"); var monthLabel = panel.Locator("[data-calendar-month]"); var initialMonth = await monthLabel.InnerTextAsync(); // Click the previous arrow await panel.Locator("button[aria-label='Previous month']").ClickAsync(); await Page.WaitForTimeoutAsync(200); var newMonth = await monthLabel.InnerTextAsync(); Assert.That(newMonth, Is.Not.EqualTo(initialMonth), "Month label should change after clicking Previous"); } [Test] public async Task Calendar_Next_Arrow_Wraps_Year() { await GoToFormsAsync(); // Open calendar await Trigger("trigger-birthdate").ClickAsync(); await Page.WaitForTimeoutAsync(300); var panel = PopoverPanelFor("trigger-birthdate"); // Navigate to Dec of current year var target = new DateOnly(DateTime.Today.Year, 12, 1); await NavigateCalendarToDate(panel, target); var yearLabel = panel.Locator("[data-calendar-year]"); var initialYear = await yearLabel.InnerTextAsync(); // Click next — should go to Jan of next year await panel.Locator("button[aria-label='Next month']").ClickAsync(); await Page.WaitForTimeoutAsync(200); var monthLabel = panel.Locator("[data-calendar-month]"); var newMonth = await monthLabel.InnerTextAsync(); var newYear = await yearLabel.InnerTextAsync(); Assert.That(newMonth.Trim(), Is.EqualTo("Jan"), "Should wrap to January"); Assert.That(int.Parse(newYear.Trim()), Is.EqualTo(int.Parse(initialYear.Trim()) + 1), "Year should increment"); } [Test] public async Task Calendar_Previous_Arrow_Wraps_Year() { await GoToFormsAsync(); // Open calendar await Trigger("trigger-birthdate").ClickAsync(); await Page.WaitForTimeoutAsync(300); var panel = PopoverPanelFor("trigger-birthdate"); var target = new DateOnly(DateTime.Today.Year, 1, 1); await NavigateCalendarToDate(panel, target); var yearLabel = panel.Locator("[data-calendar-year]"); var initialYear = await yearLabel.InnerTextAsync(); // Click previous — should go to Dec of previous year await panel.Locator("button[aria-label='Previous month']").ClickAsync(); await Page.WaitForTimeoutAsync(200); var monthLabel = panel.Locator("[data-calendar-month]"); var newMonth = await monthLabel.InnerTextAsync(); var newYear = await yearLabel.InnerTextAsync(); Assert.That(newMonth.Trim(), Is.EqualTo("Dec"), "Should wrap to December"); Assert.That(int.Parse(newYear.Trim()), Is.EqualTo(int.Parse(initialYear.Trim()) - 1), "Year should decrement"); } [Test] public async Task Calendar_Selecting_Day_Via_Arrow_Navigation() { await GoToFormsAsync(); // Open calendar await Trigger("trigger-birthdate").ClickAsync(); await Page.WaitForTimeoutAsync(300); var panel = PopoverPanelFor("trigger-birthdate"); // Navigate forward one month using arrow await panel.Locator("button[aria-label='Next month']").ClickAsync(); await Page.WaitForTimeoutAsync(200); // Read the new month/year var monthText = (await panel.Locator("[data-calendar-month]").InnerTextAsync()).Trim(); var yearText = (await panel.Locator("[data-calendar-year]").InnerTextAsync()).Trim(); var month = DateTime.ParseExact(monthText, "MMM", null).Month; var year = int.Parse(yearText); // Click day 15 var dayGrid = panel.Locator(".grid.grid-cols-7").Last; await dayGrid.Locator("button:not([disabled])").Filter(new LocatorFilterOptions { HasTextString = "15" }).First.ClickAsync(); await Page.WaitForTimeoutAsync(300); // Verify the hidden input has the correct value var expected = new DateOnly(year, month, 15).ToString("yyyy-MM-dd"); await Expect(Input("input-birthdate")).ToHaveValueAsync(expected); } // ════════════════════════════════════════════════════════════════════════ // DateInput: Trigger text updates after selection // ════════════════════════════════════════════════════════════════════════ [Test] public async Task DateInput_Trigger_Shows_Placeholder_Initially() { await GoToFormsAsync(); var triggerSpan = Trigger("trigger-birthdate").Locator("span"); var text = await triggerSpan.InnerTextAsync(); Assert.That(text.Trim(), Is.EqualTo("Select date"), "Should show placeholder before a date is selected"); } [Test] public async Task DateInput_Trigger_Shows_Formatted_Date_After_Selection() { await GoToFormsAsync(); // Open and select June 15, 2000 await Trigger("trigger-birthdate").ClickAsync(); await Page.WaitForTimeoutAsync(300); var panel = PopoverPanelFor("trigger-birthdate"); await NavigateCalendarToDate(panel, new DateOnly(2000, 6, 15)); var dayGrid = panel.Locator(".grid.grid-cols-7").Last; await dayGrid.Locator("button:not([disabled])").Filter(new LocatorFilterOptions { HasTextString = "15" }).First.ClickAsync(); await Page.WaitForTimeoutAsync(300); // Trigger button text should now show the formatted date var triggerSpan = Trigger("trigger-birthdate").Locator("span"); var text = (await triggerSpan.InnerTextAsync()).Trim(); Assert.That(text, Is.EqualTo("June 15, 2000"), "Trigger should display the formatted selected date"); } [Test] public async Task DateInput_Trigger_Text_Loses_Placeholder_Class_After_Selection() { await GoToFormsAsync(); var triggerSpan = Trigger("trigger-birthdate").Locator("span"); // Before selection — should have muted style var classBefore = await triggerSpan.GetAttributeAsync("class"); Assert.That(classBefore, Does.Contain("text-muted-foreground"), "Placeholder text should have muted class"); // Select a date await Trigger("trigger-birthdate").ClickAsync(); await Page.WaitForTimeoutAsync(300); var panel = PopoverPanelFor("trigger-birthdate"); await NavigateCalendarToDate(panel, new DateOnly(2000, 6, 15)); var dayGrid = panel.Locator(".grid.grid-cols-7").Last; await dayGrid.Locator("button:not([disabled])").Filter(new LocatorFilterOptions { HasTextString = "15" }).First.ClickAsync(); await Page.WaitForTimeoutAsync(300); // After selection — should NOT have muted class var classAfter = await triggerSpan.GetAttributeAsync("class"); Assert.That(classAfter, Does.Not.Contain("text-muted-foreground"), "Selected date text should not have muted class"); } // ════════════════════════════════════════════════════════════════════════ // TimeInput: Trigger text updates after selection // ════════════════════════════════════════════════════════════════════════ [Test] public async Task TimeInput_Trigger_Shows_Placeholder_Initially() { await GoToFormsAsync(); var triggerSpan = Trigger("trigger-preferredtime").Locator("span"); var text = await triggerSpan.InnerTextAsync(); Assert.That(text.Trim(), Is.EqualTo("Select time"), "Should show placeholder before a time is selected"); } [Test] public async Task TimeInput_Trigger_Shows_Formatted_Time_After_Selection() { await GoToFormsAsync(); // Open time picker and select 2:30 PM await Trigger("trigger-preferredtime").ClickAsync(); await Page.WaitForTimeoutAsync(300); var panel = PopoverPanelFor("trigger-preferredtime"); await PickTimeInOpenPopover(panel, 14, 30); // Close by clicking backdrop var backdrop = PopoverBackdropFor("trigger-preferredtime"); await backdrop.ClickAsync(new LocatorClickOptions { Force = true }); await Page.WaitForTimeoutAsync(300); var triggerSpan = Trigger("trigger-preferredtime").Locator("span"); var text = (await triggerSpan.InnerTextAsync()).Trim(); Assert.That(text, Is.EqualTo("02:30 PM"), "Trigger should display the formatted selected time"); } [Test] public async Task TimeInput_Trigger_Text_Loses_Placeholder_Class_After_Selection() { await GoToFormsAsync(); var triggerSpan = Trigger("trigger-preferredtime").Locator("span"); // Before selection var classBefore = await triggerSpan.GetAttributeAsync("class"); Assert.That(classBefore, Does.Contain("text-muted-foreground"), "Placeholder should have muted class"); // Select a time await Trigger("trigger-preferredtime").ClickAsync(); await Page.WaitForTimeoutAsync(300); var panel = PopoverPanelFor("trigger-preferredtime"); await PickTimeInOpenPopover(panel, 14, 30); var backdrop = PopoverBackdropFor("trigger-preferredtime"); await backdrop.ClickAsync(new LocatorClickOptions { Force = true }); await Page.WaitForTimeoutAsync(300); // After selection var classAfter = await triggerSpan.GetAttributeAsync("class"); Assert.That(classAfter, Does.Not.Contain("text-muted-foreground"), "Selected time text should not have muted class"); } // ════════════════════════════════════════════════════════════════════════ // DateTimeInput: Trigger text updates after selection // ════════════════════════════════════════════════════════════════════════ [Test] public async Task DateTimeInput_Date_Trigger_Shows_Placeholder_Initially() { await GoToFormsAsync(); var triggerSpan = Trigger("trigger-appointment-date").Locator("span"); var text = await triggerSpan.InnerTextAsync(); Assert.That(text.Trim(), Is.EqualTo("Select date"), "Date trigger should show placeholder initially"); } [Test] public async Task DateTimeInput_Time_Trigger_Shows_Placeholder_Initially() { await GoToFormsAsync(); var triggerSpan = Trigger("trigger-appointment-time").Locator("span"); var text = await triggerSpan.InnerTextAsync(); Assert.That(text.Trim(), Is.EqualTo("Select time"), "Time trigger should show placeholder initially"); } [Test] public async Task DateTimeInput_Date_Trigger_Shows_Formatted_Date_After_Selection() { await GoToFormsAsync(); // Open and select Dec 25, 2025 await Trigger("trigger-appointment-date").ClickAsync(); await Page.WaitForTimeoutAsync(300); var panel = PopoverPanelFor("trigger-appointment-date"); await NavigateCalendarToDate(panel, new DateOnly(2025, 12, 25)); var dayGrid = panel.Locator(".grid.grid-cols-7").Last; await dayGrid.Locator("button:not([disabled])").Filter(new LocatorFilterOptions { HasTextString = "25" }).First.ClickAsync(); await Page.WaitForTimeoutAsync(300); var triggerSpan = Trigger("trigger-appointment-date").Locator("span"); var text = (await triggerSpan.InnerTextAsync()).Trim(); Assert.That(text, Is.EqualTo("Dec 25, 2025"), "Date trigger should display the formatted selected date"); } [Test] public async Task DateTimeInput_Time_Trigger_Shows_Formatted_Time_After_Selection() { await GoToFormsAsync(); // Must select a date first so the component has a value await Trigger("trigger-appointment-date").ClickAsync(); await Page.WaitForTimeoutAsync(300); var datePanel = PopoverPanelFor("trigger-appointment-date"); await NavigateCalendarToDate(datePanel, new DateOnly(2025, 12, 25)); var dayGrid = datePanel.Locator(".grid.grid-cols-7").Last; await dayGrid.Locator("button:not([disabled])").Filter(new LocatorFilterOptions { HasTextString = "25" }).First.ClickAsync(); await Page.WaitForTimeoutAsync(300); // Now select time 10:00 AM await Trigger("trigger-appointment-time").ClickAsync(); await Page.WaitForTimeoutAsync(300); var timePanel = PopoverPanelFor("trigger-appointment-time"); await PickTimeInOpenPopover(timePanel, 10, 0); var backdrop = PopoverBackdropFor("trigger-appointment-time"); await backdrop.ClickAsync(new LocatorClickOptions { Force = true }); await Page.WaitForTimeoutAsync(300); var triggerSpan = Trigger("trigger-appointment-time").Locator("span"); var text = (await triggerSpan.InnerTextAsync()).Trim(); Assert.That(text, Is.EqualTo("10:00 AM"), "Time trigger should display the formatted selected time"); } // ════════════════════════════════════════════════════════════════════════ // Calendar: day selection highlights correctly // ════════════════════════════════════════════════════════════════════════ [Test] public async Task Calendar_Selected_Day_Has_Primary_Styling() { await GoToFormsAsync(); // Open and select a date await Trigger("trigger-birthdate").ClickAsync(); await Page.WaitForTimeoutAsync(300); var panel = PopoverPanelFor("trigger-birthdate"); var dayGrid = panel.Locator(".grid.grid-cols-7").Last; var day15 = dayGrid.Locator("button:not([disabled])").Filter(new LocatorFilterOptions { HasTextString = "15" }).First; await day15.ClickAsync(); await Page.WaitForTimeoutAsync(300); // Re-open the calendar to verify the selected day is highlighted await Trigger("trigger-birthdate").ClickAsync(); await Page.WaitForTimeoutAsync(300); panel = PopoverPanelFor("trigger-birthdate"); var selectedDay = panel.Locator(".grid.grid-cols-7").Last .Locator("button:not([disabled])").Filter(new LocatorFilterOptions { HasTextString = "15" }).First; var cls = await selectedDay.GetAttributeAsync("class"); Assert.That(cls, Does.Contain("bg-primary"), "Selected day should have primary background styling"); } // ════════════════════════════════════════════════════════════════════════ // Popover: Date selection auto-closes popover // ════════════════════════════════════════════════════════════════════════ [Test] public async Task DateInput_Popover_Closes_After_Day_Selection() { await GoToFormsAsync(); await Trigger("trigger-birthdate").ClickAsync(); await Page.WaitForTimeoutAsync(300); var panel = PopoverPanelFor("trigger-birthdate"); await Expect(panel).ToBeVisibleAsync(); // Select a day var dayGrid = panel.Locator(".grid.grid-cols-7").Last; await dayGrid.Locator("button:not([disabled])").Filter(new LocatorFilterOptions { HasTextString = "10" }).First.ClickAsync(); await Page.WaitForTimeoutAsync(400); // Popover should auto-close after date selection await Expect(panel).ToBeHiddenAsync(); } }