Actions and assertions
Everything you do to a page goes through a WebElement.
Actions
ValueTask ClickAsync(CancellationToken cancellationToken = default);
ValueTask FillAsync(string value, CancellationToken cancellationToken = default);
ValueTask CheckAsync(CancellationToken cancellationToken = default);
ValueTask UncheckAsync(CancellationToken cancellationToken = default);
ValueTask SelectOptionAsync(string value, CancellationToken cancellationToken = default); // matches the option's value attribute
ValueTask PressAsync(WebKey key, CancellationToken cancellationToken = default);
WebKey has Enter, Tab, Escape, Space, Backspace, Delete, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Home, End, PageUp and PageDown. Both backends read one shared WebKeyMap, so a key means the same thing on each.
await page.Search.FillAsync("invoice");
await page.Search.PressAsync(WebKey.Enter);
await page.Language.SelectOptionAsync("nl");
await page.RememberMe.CheckAsync();
How actions wait
- Playwright keeps its own auto-waiting: an action waits until the element is attached, visible, stable and enabled. ProtoTest resolves the locator and calls the native action; it never re-issues a failed one.
- Selenium retries for up to
ActionTimeout(5 s by default), polling everyPollInterval, until the element is displayed and enabled — and, for fills and selects, not read-only; for clicks and checks, not moving (whenWaitForStableBounds) and not covered by another element (whenCheckClickObstructionand the driver supports JavaScript). Otherwise it throwsWebActionabilityExceptionwith the component path, locator and last observation.
To wait for something application-specific — a spinner, a pending XHR — add a wait condition.
Downloads
A test can capture a file the browser downloads and check its contents, without knowing where the browser would have put it:
public sealed class ReportPage : WebPage
{
public WebElement Export => Element(By.TestId("export"));
}
var page = Proto.Context.Web().Page<ReportPage>();
await page.OpenAsync("/reports");
var download = await page.DownloadAsync(
ct => page.Export.ClickAsync(ct).AsTask(), // the trigger that starts the download
name: "monthly-report.csv", // optional; defaults to the browser's suggested name
timeout: TimeSpan.FromSeconds(15)); // optional; defaults to the backend's own wait
Encoding.UTF8.GetString(download.Content.Span); // "name,total\natlas,42"
download.FileName; // monthly-report.csv
download.MediaType; // text/csv (guessed from the extension)
download.Size; // bytes
WebSession.DownloadAsync and its WebPage shortcut run the trigger through the normal operation pipeline, wait for the file, return a WebDownload and register the file as a test attachment, so it reaches the runner's output and the .prototrace archive. A trigger that runs other session operations (like the semantic click above) nests them inside the download's operation. An explicit name replaces the browser's suggested file name for the record and the attachment; its extension refines the media type guess. A non-positive timeout throws ArgumentOutOfRangeException.
- Playwright captures natively: the trigger runs, Playwright waits for the download, and ProtoTest reads the completed file. This covers link, form and generated (
blob:,data:) downloads. - Selenium has no download API in the WebDriver protocol, so
DownloadAsyncthrowsWebBackendCapabilityExceptionbefore the trigger runs, naming the limitation. Fetch the file over HTTP with ProtoTest.Rest instead.
Reading state
ValueTask<string> TextAsync(CancellationToken cancellationToken = default);
ValueTask<string?> ValueAsync(CancellationToken cancellationToken = default);
ValueTask<bool> IsVisibleAsync(CancellationToken cancellationToken = default);
ValueTask<bool> IsEnabledAsync(CancellationToken cancellationToken = default);
ValueTask<bool> IsCheckedAsync(CancellationToken cancellationToken = default);
These read once, right now. For checks in a test, prefer the assertions below — they retry.
Assertions
Assertions hang off Should (positive) and ShouldNot (negated), both returning a WebAssertions:
public sealed class WebElement
{
public WebAssertions Should { get; } // the assertion must hold
public WebAssertions ShouldNot { get; } // the assertion must not hold
}
public sealed class WebAssertions
{
ValueTask BeVisibleAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default);
ValueTask BeEnabledAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default);
ValueTask BeCheckedAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default);
ValueTask HaveTextAsync(string expected, TimeSpan? timeout = null, CancellationToken cancellationToken = default);
ValueTask ContainTextAsync(string expected, TimeSpan? timeout = null, CancellationToken cancellationToken = default);
ValueTask HaveValueAsync(string expected, TimeSpan? timeout = null, CancellationToken cancellationToken = default);
}
await page.Save.Should.BeEnabledAsync(TimeSpan.FromSeconds(2));
await page.Save.ClickAsync();
await page.Status.Should.HaveTextAsync("saved");
await page.Error.ShouldNot.BeVisibleAsync();
Assertions poll: every 50 ms until the condition holds or the timeout passes (5 seconds when you don't pass one). While polling, "element not found yet" and "not actionable yet" are treated as "not yet", not as failures. When time runs out you get a WebAssertionException describing the last thing observed.
HaveTextAsyncis an exact, ordinal comparison;ContainTextAsyncchecks for an ordinal substring.ShouldNotis the inverse of each.HaveValueAsynccompares the value ordinally, but the failure message reports only the value's length.- A timeout of zero or less throws
ArgumentOutOfRangeException.
Trace evidence
Each assertion is a assert.web operation on the element, with web.expectation, web.assert.negated and web.assert.timeout attributes. A passing assertion records a web.page.verified coverage observation for the page it was checked on — that is what makes a page count as covered.
FillAsync records only the length of what was typed (web.value is [REDACTED]), and HaveValueAsync failures report the value's length rather than the value. Passwords and personal data you type in tests never end up in a .prototrace file.
Element metadata
string Name { get; } // e.g. "Submit"
string ComponentPath { get; } // e.g. "LoginPage.Form.Submit"
WebLocator Locator { get; }
WebElementReference Reference { get; } // what WaitUntilAsync and custom waits consume
Next
- Flows — group actions on a component into one named operation.
- Diagnostics and artifacts — what a failed action leaves behind.