Files
Enciphered.Blazor.UIComponents/Enciphered.Blazor.UIComponents.Tests/FormsTests.cs
T
2026-04-13 15:08:49 +05:00

606 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Playwright;
namespace Enciphered.Blazor.UIComponents.Tests;
[TestFixture]
public class FormsTests : PlaywrightTestBase
{
// ── Helpers ──────────────────────────────────────────────────────────────
private async Task GoToFormsAsync()
{
await Page.GotoAsync($"{BaseUrl}/forms", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle });
// Wait for Blazor interactive mode to be ready
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}']");
private ILocator Btn(string testId) => Page.Locator($"[data-testid='{testId}']");
/// <summary>
/// Select a date via the calendar popover.
/// Opens the trigger, uses month/year pickers to navigate, then clicks the day.
/// </summary>
private async Task SelectDateAsync(string triggerId, DateOnly target)
{
// Open the popover
await Trigger(triggerId).ClickAsync();
await Page.WaitForTimeoutAsync(200);
await NavigateCalendarToDate(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;
var dayButton = dayGrid.Locator($"button:not([disabled])").Filter(new LocatorFilterOptions { HasTextString = target.Day.ToString() }).First;
await dayButton.ClickAsync();
await Page.WaitForTimeoutAsync(200);
}
/// <summary>
/// Navigate the open calendar to a specific month/year using the month and year pickers.
/// </summary>
private async Task NavigateCalendarToDate(DateOnly target)
{
// Click year header to open year picker, then select the year
var yearButton = Page.Locator("[data-calendar-year]");
await yearButton.ClickAsync();
await Page.WaitForTimeoutAsync(100);
// The year picker is a scrollable grid; find and click the target year
var yearGrid = Page.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)
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();
else
await Page.Locator("button[aria-label='Next month']").ClickAsync();
await Page.WaitForTimeoutAsync(50);
yearGrid = Page.Locator(".grid.grid-cols-4");
targetYearBtn = yearGrid.Locator($"button:has-text('{target.Year}')");
attempts++;
}
await targetYearBtn.First.ClickAsync();
await Page.WaitForTimeoutAsync(100);
// Now click month header to open month picker, then select the month
var monthButton = Page.Locator("[data-calendar-month]");
await monthButton.ClickAsync();
await Page.WaitForTimeoutAsync(100);
var monthGrid = Page.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);
}
/// <summary>
/// Select a time via the time picker popover.
/// Opens the trigger, clicks the hour, minute, and AM/PM.
/// </summary>
private async Task SelectTimeAsync(string triggerId, int hour, int minute)
{
// Open the popover
await Trigger(triggerId).ClickAsync();
await Page.WaitForTimeoutAsync(200);
await PickTimeInOpenPopover(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);
}
/// <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)
{
// 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)
var hourText = hour12.ToString("D2");
var hourColumn = popoverContent.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)
var minuteText = minute.ToString("D2");
var minuteColumn = popoverContent.Locator(".scrollbar-thin").Nth(1);
await minuteColumn.Locator($"button:has-text('{minuteText}')").First.ClickAsync();
await Page.WaitForTimeoutAsync(50);
// Click AM/PM (within the popover)
var periodText = isPm ? "PM" : "AM";
await popoverContent.Locator($"button:has-text('{periodText}')").First.ClickAsync();
await Page.WaitForTimeoutAsync(50);
}
// ════════════════════════════════════════════════════════════════════════
// Rendering
// ════════════════════════════════════════════════════════════════════════
[Test]
public async Task FormsPage_Loads_Successfully()
{
var response = await Page.GotoAsync($"{BaseUrl}/forms");
Assert.That(response!.Status, Is.EqualTo(200));
}
[Test]
public async Task FormsPage_Has_Title()
{
await GoToFormsAsync();
var heading = Page.Locator("[data-sidebar-inset] h1.text-3xl");
await Expect(heading).ToHaveTextAsync("Forms Demo");
}
[Test]
public async Task All_Inputs_Are_Rendered()
{
await GoToFormsAsync();
await Expect(Input("input-name")).ToBeVisibleAsync();
await Expect(Input("input-email")).ToBeVisibleAsync();
await Expect(Input("input-password")).ToBeVisibleAsync();
await Expect(Input("input-age")).ToBeVisibleAsync();
// Date/Time/DateTime use popover triggers instead of visible native inputs
await Expect(Trigger("trigger-birthdate")).ToBeVisibleAsync();
await Expect(Trigger("trigger-preferredtime")).ToBeVisibleAsync();
await Expect(Trigger("trigger-appointment-date")).ToBeVisibleAsync();
await Expect(Trigger("trigger-appointment-time")).ToBeVisibleAsync();
}
[Test]
public async Task All_Buttons_Are_Rendered()
{
await GoToFormsAsync();
await Expect(Btn("btn-submit")).ToBeVisibleAsync();
await Expect(Btn("btn-reset")).ToBeVisibleAsync();
await Expect(Btn("btn-disabled")).ToBeVisibleAsync();
}
// ════════════════════════════════════════════════════════════════════════
// Input types
// ════════════════════════════════════════════════════════════════════════
[Test]
public async Task TextInput_Has_Correct_Type()
{
await GoToFormsAsync();
await Expect(Input("input-name")).ToHaveAttributeAsync("type", "text");
}
[Test]
public async Task EmailInput_Has_Correct_Type()
{
await GoToFormsAsync();
await Expect(Input("input-email")).ToHaveAttributeAsync("type", "email");
}
[Test]
public async Task PasswordInput_Has_Correct_Type()
{
await GoToFormsAsync();
await Expect(Input("input-password")).ToHaveAttributeAsync("type", "password");
}
[Test]
public async Task NumberInput_Has_Correct_Type()
{
await GoToFormsAsync();
await Expect(Input("input-age")).ToHaveAttributeAsync("type", "number");
}
[Test]
public async Task DateInput_Has_Correct_Type()
{
await GoToFormsAsync();
await Expect(Input("input-birthdate")).ToHaveAttributeAsync("type", "date");
}
[Test]
public async Task TimeInput_Has_Correct_Type()
{
await GoToFormsAsync();
await Expect(Input("input-time")).ToHaveAttributeAsync("type", "time");
}
[Test]
public async Task DateTimeInput_Has_Correct_Type()
{
await GoToFormsAsync();
await Expect(Input("input-appointment")).ToHaveAttributeAsync("type", "datetime-local");
}
// ════════════════════════════════════════════════════════════════════════
// Placeholders
// ════════════════════════════════════════════════════════════════════════
[Test]
public async Task TextInput_Shows_Placeholder()
{
await GoToFormsAsync();
await Expect(Input("input-name")).ToHaveAttributeAsync("placeholder", "Jane Doe");
}
[Test]
public async Task EmailInput_Shows_Placeholder()
{
await GoToFormsAsync();
await Expect(Input("input-email")).ToHaveAttributeAsync("placeholder", "jane@example.com");
}
// ════════════════════════════════════════════════════════════════════════
// Labels
// ════════════════════════════════════════════════════════════════════════
[Test]
public async Task FormFields_Have_Labels()
{
await GoToFormsAsync();
var labels = Page.Locator("label");
var count = await labels.CountAsync();
Assert.That(count, Is.EqualTo(7), "Expected 7 labels (one per form field)");
}
[Test]
public async Task Label_For_Attribute_Matches_Input_Id()
{
await GoToFormsAsync();
var label = Page.Locator("label[for='name']");
await Expect(label).ToHaveTextAsync("Full Name");
var input = Input("input-name");
await Expect(input).ToHaveAttributeAsync("id", "name");
}
// ════════════════════════════════════════════════════════════════════════
// Two-way binding
// ════════════════════════════════════════════════════════════════════════
[Test]
public async Task TextInput_Binds_Value()
{
await GoToFormsAsync();
var input = Input("input-name");
await input.FillAsync("Alice");
await Expect(input).ToHaveValueAsync("Alice");
}
[Test]
public async Task NumberInput_Binds_Value()
{
await GoToFormsAsync();
var input = Input("input-age");
await input.FillAsync("30");
await Expect(input).ToHaveValueAsync("30");
}
[Test]
public async Task DateInput_Binds_Value()
{
await GoToFormsAsync();
// Use the calendar popover to select June 15, 2000
await SelectDateAsync("trigger-birthdate", new DateOnly(2000, 6, 15));
// The hidden input should reflect the selected date
await Expect(Input("input-birthdate")).ToHaveValueAsync("2000-06-15");
}
[Test]
public async Task TimeInput_Binds_Value()
{
await GoToFormsAsync();
// Use the time picker popover to select 14:30 (2:30 PM)
await SelectTimeAsync("trigger-preferredtime", 14, 30);
// The hidden input should reflect the selected time
await Expect(Input("input-time")).ToHaveValueAsync("14:30");
}
[Test]
public async Task DateTimeInput_Binds_Value()
{
await GoToFormsAsync();
// Pick the date part via the date trigger
await SelectDateAsync("trigger-appointment-date", new DateOnly(2025, 12, 25));
// 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 = "2100 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 = "864 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();
}
// ════════════════════════════════════════════════════════════════════════
// Button variants
// ════════════════════════════════════════════════════════════════════════
[Test]
public async Task Submit_Button_Has_Default_Variant_Classes()
{
await GoToFormsAsync();
var btn = Btn("btn-submit");
var cls = await btn.GetAttributeAsync("class");
Assert.That(cls, Does.Contain("bg-primary"), "Submit should use default variant");
}
[Test]
public async Task Reset_Button_Has_Outline_Variant_Classes()
{
await GoToFormsAsync();
var btn = Btn("btn-reset");
var cls = await btn.GetAttributeAsync("class");
Assert.That(cls, Does.Contain("border"), "Reset should use outline variant");
Assert.That(cls, Does.Contain("bg-background"), "Reset should use outline variant");
}
[Test]
public async Task Disabled_Button_Is_Actually_Disabled()
{
await GoToFormsAsync();
var btn = Btn("btn-disabled");
await Expect(btn).ToBeDisabledAsync();
}
[Test]
public async Task Disabled_Button_Has_Destructive_Variant()
{
await GoToFormsAsync();
var btn = Btn("btn-disabled");
var cls = await btn.GetAttributeAsync("class");
Assert.That(cls, Does.Contain("bg-destructive"), "Disabled button should have destructive variant");
}
// ════════════════════════════════════════════════════════════════════════
// Reset
// ════════════════════════════════════════════════════════════════════════
[Test]
public async Task Reset_Button_Clears_Form()
{
await GoToFormsAsync();
// Fill some fields
await Input("input-name").FillAsync("Alice");
await Input("input-email").FillAsync("alice@test.com");
// Reset
await Btn("btn-reset").ClickAsync();
// Fields should be empty
await Expect(Input("input-name")).ToHaveValueAsync("");
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)
// ════════════════════════════════════════════════════════════════════════
[Test]
public async Task Inputs_Have_Base_Styling_Classes()
{
await GoToFormsAsync();
var input = Input("input-name");
var cls = await input.GetAttributeAsync("class");
Assert.That(cls, Does.Contain("rounded-md"), "Input should have rounded-md class");
Assert.That(cls, Does.Contain("border"), "Input should have border class");
}
// ════════════════════════════════════════════════════════════════════════
// Navigation to forms page
// ════════════════════════════════════════════════════════════════════════
[Test]
public async Task Sidebar_Has_Forms_Link()
{
await Page.GotoAsync(BaseUrl, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle });
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state]", new PageWaitForSelectorOptions { Timeout = 10_000 });
var formsLink = Page.Locator("a[href='/forms']");
await Expect(formsLink).ToBeVisibleAsync();
}
[Test]
public async Task Navigate_To_Forms_Via_Sidebar()
{
await Page.GotoAsync(BaseUrl, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle });
await Page.WaitForSelectorAsync("[data-sidebar-wrapper][data-state]", new PageWaitForSelectorOptions { Timeout = 10_000 });
await Page.Locator("a[href='/forms']").ClickAsync();
await Page.WaitForURLAsync($"{BaseUrl}/forms");
var heading = Page.Locator("[data-sidebar-inset] h1.text-3xl");
await Expect(heading).ToHaveTextAsync("Forms Demo");
}
}