Testing components
Test a component's logic as plain F#, with no browser, and the component itself in a real browser with Playwright, from the same .NET test project style.
This recipe has no demo: its code is tests. They test the Form validation
recipe's sign-up form. This site runs a longer version of the browser test on that demo, in
tests/Site.E2E, along with one
for every other recipe here.
The logic, without a browser
Keep the decisions in functions that don't touch Lit or the DOM, in a file of their own: the
model, update, validate. They're ordinary F#, so they compile for .NET as well as for
Fable, and a test runs them in milliseconds:
module Signup
open System.Text.RegularExpressions
type Signup = { Name: string; Email: string }
let validate (form: Signup) =
[
if form.Name.Trim() = "" then
"name", "Enter your name."
if not (Regex.IsMatch(form.Email.Trim(), @"^[^@\s]+@[^@\s]+\.[^@\s]+$")) then
"email", "Enter an email address, such as ada@example.com."
]
open Expecto
open Signup
let tests =
testList "validate" [
test "an empty form has both errors, in order" {
let errors = validate { Name = ""; Email = "" }
Expect.equal (List.map fst errors) [ "name"; "email" ] "fields with errors"
}
test "an address needs a domain" {
let errors = validate { Name = "Ada"; Email = "ada@" }
Expect.equal (List.map fst errors) [ "email" ] "fields with errors"
}
]
The component's file then opens Signup and does only the rendering. The
Kanban sample's tests test
its Elmish update this way, with Pyxpecto, which runs the same tests under .NET and, compiled by
Fable, in Node.
The component, in a browser
What the user sees, the focus, and the accessible names need a browser. Playwright for .NET drives Chromium, Firefox or WebKit from an F# test:
dotnet add package Microsoft.Playwright
dotnet build
pwsh bin/Debug/net10.0/playwright.ps1 install chromium
Serve the built app, as vite preview does, and point the test at it:
open Expecto
open Microsoft.Playwright
open type Microsoft.Playwright.Assertions
let signupForm =
testTask "the sign-up form shows its errors, focuses the first, then thanks" {
let! (playwright: IPlaywright) = Playwright.CreateAsync()
try
use! (browser: IBrowser) = playwright.Chromium.LaunchAsync()
let! (page: IPage) = browser.NewPageAsync()
let! _ = page.GotoAsync "http://localhost:4173/"
let form = page.Locator "my-signup-form"
let name = form.GetByLabel "Name"
let signUp = form.GetByRole(AriaRole.Button, LocatorGetByRoleOptions(Name = "Sign up"))
do! signUp.ClickAsync()
do! Expect(name).ToBeFocusedAsync()
do! Expect(name).ToHaveAccessibleDescriptionAsync "Enter your name."
do! name.FillAsync "Ada"
do! form.GetByLabel("Email").FillAsync "ada@example.com"
do! signUp.ClickAsync()
do! Expect(form.GetByRole AriaRole.Status).ToHaveTextAsync "Thanks, Ada. Check your inbox."
finally
playwright.Dispose()
}
- Shadow DOM. Playwright's locators look inside open shadow roots, so
GetByLabel "Name"finds the<input>in the component's shadow root. Lit's shadow roots are open unless you say otherwise. XPath locators don't look inside. - Find things as the user does.
GetByRoleandGetByLabelfind elements by their role and accessible name, so the test also checks that the button has a name and the field a label.ToHaveAccessibleDescriptionAsyncchecks thearia-describedbylink. - No sleeps. Lit renders asynchronously, after the click. Every
Expectretries until it passes or times out, five seconds by default, so the test waits exactly as long as the render takes. - More checks for free. In the same test, listen to
page.Consolefor errors, and run axe withDeque.AxeCore.Playwright'spage.RunAxe ()for accessibility rules. This site's tests do both on every page.
use! closes the browser when the test ends, whether it passes or fails, and finally stops
Playwright's driver. Expecto's testTask takes use and use! only for IAsyncDisposable,
which the browser is and Playwright isn't. In a suite, start Playwright and the browser once and share them,
as Site.E2E does, and give each test a new page.
Related
- Prerendering components at build time, which has its own test: with JavaScript off, a prerendered component should still show its first render.