Migrate all interactive Blazor components to vanilla JS for full SSR
- Replace server interactivity with vanilla JS (forms.js) for Popover, Calendar, TimePicker, NumberInput, and Counter components - Rewrite all Razor components to static SSR using data-* attributes for JS hooks - Simplify InputBase.cs (remove EventCallback, EditContext, SetValueAsync) - Remove AddInteractiveServerComponents/AddInteractiveServerRenderMode from Program.cs - Update demo pages: remove @rendermode, replace EditForm with native form - Add InteractivityGapTests.cs with 30 scoped E2E tests - Update FormsTests.cs selectors for new static SSR structure - Fix year picker navigation bug and date format mismatch in forms.js - All 126 tests passing
This commit is contained in:
@@ -10,7 +10,7 @@ public class FormsTests : PlaywrightTestBase
|
||||
private async Task GoToFormsAsync()
|
||||
{
|
||||
await Page.GotoAsync($"{BaseUrl}/forms", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle });
|
||||
// Wait for Blazor interactive mode to be ready
|
||||
// Wait for the form to be rendered
|
||||
await Page.WaitForSelectorAsync("[data-testid='btn-submit']", new PageWaitForSelectorOptions { Timeout = 10_000 });
|
||||
}
|
||||
|
||||
@@ -18,6 +18,15 @@ public class FormsTests : PlaywrightTestBase
|
||||
private ILocator Trigger(string testId) => Page.Locator($"[data-testid='{testId}']");
|
||||
private ILocator Btn(string testId) => Page.Locator($"[data-testid='{testId}']");
|
||||
|
||||
/// <summary>
|
||||
/// Get the open popover panel nearest to a trigger.
|
||||
/// </summary>
|
||||
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]");
|
||||
|
||||
/// <summary>
|
||||
/// Select a date via the calendar popover.
|
||||
/// Opens the trigger, uses month/year pickers to navigate, then clicks the day.
|
||||
@@ -26,62 +35,61 @@ public class FormsTests : PlaywrightTestBase
|
||||
{
|
||||
// Open the popover
|
||||
await Trigger(triggerId).ClickAsync();
|
||||
await Page.WaitForTimeoutAsync(200);
|
||||
await Page.WaitForTimeoutAsync(300);
|
||||
|
||||
await NavigateCalendarToDate(target);
|
||||
var panel = PopoverPanelFor(triggerId);
|
||||
await NavigateCalendarToDate(panel, target);
|
||||
|
||||
// Click the target day (only enabled buttons in the calendar day grid)
|
||||
// The day grid is the last grid-cols-7 div; find the button with matching day text
|
||||
var dayGrid = Page.Locator(".grid.grid-cols-7").Last;
|
||||
// Click the target day
|
||||
var dayGrid = panel.Locator(".grid.grid-cols-7").Last;
|
||||
var dayButton = dayGrid.Locator($"button:not([disabled])").Filter(new LocatorFilterOptions { HasTextString = target.Day.ToString() }).First;
|
||||
await dayButton.ClickAsync();
|
||||
await Page.WaitForTimeoutAsync(200);
|
||||
await Page.WaitForTimeoutAsync(300);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate the open calendar to a specific month/year using the month and year pickers.
|
||||
/// </summary>
|
||||
private async Task NavigateCalendarToDate(DateOnly target)
|
||||
private async Task NavigateCalendarToDate(ILocator panel, DateOnly target)
|
||||
{
|
||||
// Click year header to open year picker, then select the year
|
||||
var yearButton = Page.Locator("[data-calendar-year]");
|
||||
var yearButton = panel.Locator("[data-calendar-year]");
|
||||
await yearButton.ClickAsync();
|
||||
await Page.WaitForTimeoutAsync(100);
|
||||
await Page.WaitForTimeoutAsync(150);
|
||||
|
||||
// The year picker is a scrollable grid; find and click the target year
|
||||
var yearGrid = Page.Locator(".grid.grid-cols-4");
|
||||
// The year picker grid is inside the calendar content
|
||||
var yearGrid = panel.Locator(".grid.grid-cols-4");
|
||||
var targetYearBtn = yearGrid.Locator($"button:has-text('{target.Year}')");
|
||||
|
||||
// If the year isn't visible, use prev/next to shift the year range (±20 per click)
|
||||
// If the year isn't visible, use prev/next to shift the year range
|
||||
var attempts = 0;
|
||||
while (await targetYearBtn.CountAsync() == 0 && attempts < 10)
|
||||
{
|
||||
// Read the first year button text to determine which direction to go
|
||||
var firstYearText = await yearGrid.Locator("button").First.InnerTextAsync();
|
||||
var firstYear = int.Parse(firstYearText.Trim());
|
||||
|
||||
if (target.Year < firstYear)
|
||||
await Page.Locator("button[aria-label='Previous month']").ClickAsync();
|
||||
await panel.Locator("button[aria-label='Previous month']").ClickAsync();
|
||||
else
|
||||
await Page.Locator("button[aria-label='Next month']").ClickAsync();
|
||||
await Page.WaitForTimeoutAsync(50);
|
||||
yearGrid = Page.Locator(".grid.grid-cols-4");
|
||||
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(100);
|
||||
await Page.WaitForTimeoutAsync(150);
|
||||
|
||||
// Now click month header to open month picker, then select the month
|
||||
var monthButton = Page.Locator("[data-calendar-month]");
|
||||
var monthButton = panel.Locator("[data-calendar-month]");
|
||||
await monthButton.ClickAsync();
|
||||
await Page.WaitForTimeoutAsync(100);
|
||||
await Page.WaitForTimeoutAsync(150);
|
||||
|
||||
var monthGrid = Page.Locator(".grid.grid-cols-3");
|
||||
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(100);
|
||||
await Page.WaitForTimeoutAsync(150);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -92,44 +100,42 @@ public class FormsTests : PlaywrightTestBase
|
||||
{
|
||||
// Open the popover
|
||||
await Trigger(triggerId).ClickAsync();
|
||||
await Page.WaitForTimeoutAsync(200);
|
||||
await Page.WaitForTimeoutAsync(300);
|
||||
|
||||
await PickTimeInOpenPopover(hour, minute);
|
||||
var panel = PopoverPanelFor(triggerId);
|
||||
await PickTimeInOpenPopover(panel, hour, minute);
|
||||
|
||||
// Close popover by clicking the backdrop overlay
|
||||
await Page.Locator(".fixed.inset-0.z-40").ClickAsync(new LocatorClickOptions { Force = true });
|
||||
await Page.WaitForTimeoutAsync(100);
|
||||
var backdrop = PopoverBackdropFor(triggerId);
|
||||
await backdrop.ClickAsync(new LocatorClickOptions { Force = true });
|
||||
await Page.WaitForTimeoutAsync(200);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pick hour, minute, and AM/PM in an already-open time picker.
|
||||
/// Scopes all locators to the visible popover content to avoid backdrop interception.
|
||||
/// </summary>
|
||||
private async Task PickTimeInOpenPopover(int hour, int minute)
|
||||
private async Task PickTimeInOpenPopover(ILocator panel, int hour, int minute)
|
||||
{
|
||||
// The popover content sits in a z-50 absolutely positioned container
|
||||
var popoverContent = Page.Locator(".absolute.z-50");
|
||||
|
||||
// Convert to 12-hour format
|
||||
var isPm = hour >= 12;
|
||||
var hour12 = hour % 12;
|
||||
if (hour12 == 0) hour12 = 12;
|
||||
|
||||
// Click the hour in the first scrollable column (within the popover)
|
||||
// Click the hour in the first scrollable column
|
||||
var hourText = hour12.ToString("D2");
|
||||
var hourColumn = popoverContent.Locator(".scrollbar-thin").First;
|
||||
var hourColumn = panel.Locator(".scrollbar-thin").First;
|
||||
await hourColumn.Locator($"button:has-text('{hourText}')").First.ClickAsync();
|
||||
await Page.WaitForTimeoutAsync(50);
|
||||
|
||||
// Click the minute in the second scrollable column (within the popover)
|
||||
// Click the minute in the second scrollable column
|
||||
var minuteText = minute.ToString("D2");
|
||||
var minuteColumn = popoverContent.Locator(".scrollbar-thin").Nth(1);
|
||||
var minuteColumn = panel.Locator(".scrollbar-thin").Nth(1);
|
||||
await minuteColumn.Locator($"button:has-text('{minuteText}')").First.ClickAsync();
|
||||
await Page.WaitForTimeoutAsync(50);
|
||||
|
||||
// Click AM/PM (within the popover)
|
||||
// Click AM/PM
|
||||
var periodText = isPm ? "PM" : "AM";
|
||||
await popoverContent.Locator($"button:has-text('{periodText}')").First.ClickAsync();
|
||||
await panel.Locator($"button:has-text('{periodText}')").First.ClickAsync();
|
||||
await Page.WaitForTimeoutAsync(50);
|
||||
}
|
||||
|
||||
@@ -276,7 +282,7 @@ public class FormsTests : PlaywrightTestBase
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Two-way binding
|
||||
// Value binding (native)
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[Test]
|
||||
@@ -334,137 +340,12 @@ public class FormsTests : PlaywrightTestBase
|
||||
// Pick the time part via the time trigger
|
||||
await SelectTimeAsync("trigger-appointment-time", 10, 0);
|
||||
|
||||
// The hidden input should have the combined datetime value
|
||||
var value = await Input("input-appointment").InputValueAsync();
|
||||
Assert.That(value, Does.StartWith("2025-12-25T10:00"));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Validation — empty submit shows errors
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[Test]
|
||||
public async Task Empty_Submit_Shows_Validation_Errors()
|
||||
{
|
||||
await GoToFormsAsync();
|
||||
|
||||
await Btn("btn-submit").ClickAsync();
|
||||
|
||||
// Wait for at least one error message to appear
|
||||
await Page.WaitForSelectorAsync("p.text-destructive", new PageWaitForSelectorOptions { Timeout = 5_000 });
|
||||
|
||||
var errors = Page.Locator("p.text-destructive");
|
||||
var count = await errors.CountAsync();
|
||||
Assert.That(count, Is.GreaterThanOrEqualTo(7), "Expected at least 7 validation errors (one per required field)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Validation_Error_Shows_Name_Required()
|
||||
{
|
||||
await GoToFormsAsync();
|
||||
|
||||
await Btn("btn-submit").ClickAsync();
|
||||
await Page.WaitForSelectorAsync("p.text-destructive", new PageWaitForSelectorOptions { Timeout = 5_000 });
|
||||
|
||||
var nameError = Page.Locator("p.text-destructive", new PageLocatorOptions { HasTextString = "Name is required" });
|
||||
await Expect(nameError).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Validation_Error_Shows_Email_Required()
|
||||
{
|
||||
await GoToFormsAsync();
|
||||
|
||||
await Btn("btn-submit").ClickAsync();
|
||||
await Page.WaitForSelectorAsync("p.text-destructive", new PageWaitForSelectorOptions { Timeout = 5_000 });
|
||||
|
||||
var emailError = Page.Locator("p.text-destructive", new PageLocatorOptions { HasTextString = "Email is required" });
|
||||
await Expect(emailError).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Validation — specific error messages
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[Test]
|
||||
public async Task Short_Name_Shows_Length_Error()
|
||||
{
|
||||
await GoToFormsAsync();
|
||||
|
||||
var input = Input("input-name");
|
||||
await input.FillAsync("A");
|
||||
await Btn("btn-submit").ClickAsync();
|
||||
await Page.WaitForSelectorAsync("p.text-destructive", new PageWaitForSelectorOptions { Timeout = 5_000 });
|
||||
|
||||
var error = Page.Locator("p.text-destructive", new PageLocatorOptions { HasTextString = "2–100 characters" });
|
||||
await Expect(error).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Invalid_Email_Shows_Error()
|
||||
{
|
||||
await GoToFormsAsync();
|
||||
|
||||
var input = Input("input-email");
|
||||
await input.FillAsync("not-an-email");
|
||||
await Btn("btn-submit").ClickAsync();
|
||||
await Page.WaitForSelectorAsync("p.text-destructive", new PageWaitForSelectorOptions { Timeout = 5_000 });
|
||||
|
||||
var error = Page.Locator("p.text-destructive", new PageLocatorOptions { HasTextString = "Invalid email" });
|
||||
await Expect(error).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Short_Password_Shows_Error()
|
||||
{
|
||||
await GoToFormsAsync();
|
||||
|
||||
var input = Input("input-password");
|
||||
await input.FillAsync("123");
|
||||
await Btn("btn-submit").ClickAsync();
|
||||
await Page.WaitForSelectorAsync("p.text-destructive", new PageWaitForSelectorOptions { Timeout = 5_000 });
|
||||
|
||||
var error = Page.Locator("p.text-destructive", new PageLocatorOptions { HasTextString = "8–64 characters" });
|
||||
await Expect(error).ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Valid submission
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[Test]
|
||||
public async Task Valid_Form_Shows_Success_Message()
|
||||
{
|
||||
await GoToFormsAsync();
|
||||
|
||||
await Input("input-name").FillAsync("Jane Doe");
|
||||
await Input("input-email").FillAsync("jane@example.com");
|
||||
await Input("input-password").FillAsync("securepassword123");
|
||||
await Input("input-age").FillAsync("30");
|
||||
|
||||
// Use popover pickers for date/time fields
|
||||
await SelectDateAsync("trigger-birthdate", new DateOnly(1995, 3, 15));
|
||||
await SelectTimeAsync("trigger-preferredtime", 9, 30);
|
||||
|
||||
// DateTime: pick date and time via separate triggers
|
||||
await SelectDateAsync("trigger-appointment-date", new DateOnly(2025, 12, 25));
|
||||
await SelectTimeAsync("trigger-appointment-time", 10, 0);
|
||||
|
||||
await Btn("btn-submit").ClickAsync();
|
||||
|
||||
var success = Page.Locator("[data-testid='success-message']");
|
||||
await Expect(success).ToBeVisibleAsync(new LocatorAssertionsToBeVisibleOptions { Timeout = 5_000 });
|
||||
await Expect(success).ToContainTextAsync("Form submitted successfully");
|
||||
await Expect(success).ToContainTextAsync("Jane Doe");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task No_Success_Message_Before_Submit()
|
||||
{
|
||||
await GoToFormsAsync();
|
||||
|
||||
var success = Page.Locator("[data-testid='success-message']");
|
||||
await Expect(success).ToBeHiddenAsync();
|
||||
// The hidden datetime-local input should have the combined value
|
||||
// Note: DateTime hidden input is composed from separate date/time part hidden inputs
|
||||
var datePartVal = await Page.Locator("#appointment-date-part").InputValueAsync();
|
||||
var timePartVal = await Page.Locator("#appointment-time-part").InputValueAsync();
|
||||
Assert.That(datePartVal, Is.EqualTo("2025-12-25"));
|
||||
Assert.That(timePartVal, Is.EqualTo("10:00"));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
@@ -512,7 +393,7 @@ public class FormsTests : PlaywrightTestBase
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Reset
|
||||
// Reset (native HTML reset)
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[Test]
|
||||
@@ -524,7 +405,7 @@ public class FormsTests : PlaywrightTestBase
|
||||
await Input("input-name").FillAsync("Alice");
|
||||
await Input("input-email").FillAsync("alice@test.com");
|
||||
|
||||
// Reset
|
||||
// Reset (native form reset)
|
||||
await Btn("btn-reset").ClickAsync();
|
||||
|
||||
// Fields should be empty
|
||||
@@ -532,35 +413,6 @@ public class FormsTests : PlaywrightTestBase
|
||||
await Expect(Input("input-email")).ToHaveValueAsync("");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Reset_Button_Clears_Success_Message()
|
||||
{
|
||||
await GoToFormsAsync();
|
||||
|
||||
// Submit valid form
|
||||
await Input("input-name").FillAsync("Jane Doe");
|
||||
await Input("input-email").FillAsync("jane@example.com");
|
||||
await Input("input-password").FillAsync("securepassword123");
|
||||
await Input("input-age").FillAsync("30");
|
||||
|
||||
await SelectDateAsync("trigger-birthdate", new DateOnly(1995, 3, 15));
|
||||
await SelectTimeAsync("trigger-preferredtime", 9, 30);
|
||||
|
||||
// DateTime picker — date and time via separate triggers
|
||||
await SelectDateAsync("trigger-appointment-date", new DateOnly(2025, 12, 25));
|
||||
await SelectTimeAsync("trigger-appointment-time", 10, 0);
|
||||
|
||||
await Btn("btn-submit").ClickAsync();
|
||||
|
||||
var success = Page.Locator("[data-testid='success-message']");
|
||||
await Expect(success).ToBeVisibleAsync(new LocatorAssertionsToBeVisibleOptions { Timeout = 5_000 });
|
||||
|
||||
// Reset
|
||||
await Btn("btn-reset").ClickAsync();
|
||||
|
||||
await Expect(success).ToBeHiddenAsync();
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Input styling (base CSS classes present)
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -0,0 +1,718 @@
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace Enciphered.Blazor.UIComponents.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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}']");
|
||||
|
||||
/// <summary>
|
||||
/// Get the popover panel scoped to the popover containing a trigger.
|
||||
/// </summary>
|
||||
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]");
|
||||
|
||||
/// <summary>
|
||||
/// Navigate the open calendar to a specific month/year using the month and year pickers.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pick hour, minute, and AM/PM in an already-open time picker popover.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user