Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@
- `KeyboardInterface::insertText()`
- `PageInterface::pause()`
- `ResponseInterface::headerValue()`
- `expect($locator)->toBeAttached()`

### Changed
- `Testing\Expect` delegates to `LocatorAssertions` and `PageAssertions`, sharing one auto-waiting and tracing implementation

### Fixed
- `expect()->toHaveClass()` matches the class attribute exactly
- `expect()->toBeEmpty()` evaluates input values and text content in the DOM
- `expect()->not()` applies to one assertion instead of leaking to later assertions on the same object

## [1.3.1] - 2026-08-04

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ Notes:

- The trait provides `$this->playwright`, `$this->browser`, `$this->context`, and `$this->page` properties.
- Call `setUpPlaywright()` in `setUp()` and `tearDownPlaywright()` in `tearDown()` for proper lifecycle management.
- Use `$this->expect($locator)` or `$this->expect($page)` for fluent assertions.
- Use `$this->expect($locator)` or `$this->expect($page)` for fluent assertions with auto-waiting.
- If you prefer full control, you can skip the trait and use the static `Playwright` facade directly.

## CI usage (GitHub Actions)
Expand Down
10 changes: 10 additions & 0 deletions docs/guide/assertions-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ expect($this->page->locator('.success-message'))
->toBeVisible();
```

### `.withPollInterval()`

Assertions poll every 100 milliseconds by default. Use `withPollInterval()` to
override that interval for the next assertion.

-----

## Locator Assertions
Expand All @@ -59,16 +64,21 @@ These assertions are available when you pass a `Locator` to `expect()`.

* **`toBeVisible()`**: Asserts the locator resolves to a visible element.
* **`toBeHidden()`**: Asserts the locator resolves to a hidden element.
* **`toBeAttached()`**: Asserts the locator resolves to an element in the DOM.
* **`toBeEnabled()`**: Asserts the element is enabled.
* **`toBeDisabled()`**: Asserts the element is disabled.
* **`toBeChecked()`**: Asserts a checkbox or radio button is checked.
* **`toBeEmpty()`**: Asserts the element has no value or text content.
* **`toBeFocused()`**: Asserts the element is focused.
* **`toHaveFocus()`**: Alias for `toBeFocused()`.
* **`toHaveText(string $text)`**: Asserts the element contains the given text.
* **`toHaveExactText(string $text)`**: Asserts the element's text is an exact match.
* **`toContainText(string $text)`**: An alias for `toHaveText()`.
* **`toHaveValue(string $value)`**: Asserts an input element has a specific value.
* **`toHaveAttribute(string $name, string $value)`**: Asserts the element has the given attribute and value.
* **`toHaveCSS(string $name, string $value)`**: Asserts the element has the given computed CSS style.
* **`toHaveId(string $id)`**: Asserts the element has the given ID.
* **`toHaveClass(string|array $class)`**: Asserts the complete class list matches.
* **`toHaveCount(int $count)`**: Asserts the locator resolves to a specific number of elements.

-----
Expand Down
118 changes: 118 additions & 0 deletions src/Assertions/Internal/AbstractAssertions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

/*
* This file is part of the community-maintained Playwright PHP project.
* It is not affiliated with or endorsed by Microsoft.
*
* (c) 2025-Present - Playwright PHP - https://github.com/playwright-php
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Playwright\Assertions\Internal;

use Playwright\Assertions\AssertionOptions;
use Playwright\Assertions\Failure\AssertionException;
use Playwright\Tracing\TracingInterface;

abstract class AbstractAssertions
{
private bool $negated = false;
private int $timeoutMs = Waiter::DEFAULT_TIMEOUT_MS;
private int $pollIntervalMs = 50;

public function __construct(private readonly ?TracingInterface $tracing = null)
{
}

protected function negate(): void
{
$this->negated = !$this->negated;
}

protected function setTimeout(int $timeoutMs): void
{
$this->timeoutMs = $timeoutMs;
}

protected function setPollInterval(int $pollIntervalMs): void
{
$this->pollIntervalMs = $pollIntervalMs;
}

/**
* @param callable(): bool $condition
* @param callable(): mixed|null $actualProvider
*/
protected function assertCondition(
callable $condition,
string $matcher,
?AssertionOptions $options,
string $message,
string $negatedMessage,
mixed $expected = null,
?callable $actualProvider = null,
): void {
$negated = $this->negated;
$this->negated = false;

if (null !== $this->tracing) {
$this->tracing->group(sprintf('expect(%s).%s%s', $this->subjectName(), $negated ? 'not.' : '', $matcher));
}

try {
$this->runAssertion($condition, !$negated, $options, $negated ? $negatedMessage : $message, $expected, $actualProvider);
} finally {
if (null !== $this->tracing) {
$this->tracing->groupEnd();
}
}
}

abstract protected function subjectName(): string;

/**
* @param callable(): bool $condition
* @param callable(): mixed|null $actualProvider
*/
private function runAssertion(
callable $condition,
bool $expectedResult,
?AssertionOptions $options,
string $message,
mixed $expected,
?callable $actualProvider,
): void {
$timeoutMs = null === $options || null === $options->timeoutMs ? $this->timeoutMs : $options->timeoutMs;
$pollIntervalMs = null === $options || null === $options->intervalMs ? $this->pollIntervalMs : $options->intervalMs;
$deadline = hrtime(true) + ($timeoutMs * 1_000_000);

do {
try {
if ($condition() === $expectedResult) {
return;
}
} catch (\Throwable) {
}

if (hrtime(true) < $deadline) {
usleep($pollIntervalMs * 1000);
}
} while (hrtime(true) < $deadline);

$actual = null;
if (null !== $actualProvider) {
try {
$actual = $actualProvider();
} catch (\Throwable) {
}
}

$message = null === $options || null === $options->message ? $message : $options->message;

throw new AssertionException($message, actual: $actual, expected: $expected);
}
}
Loading
Loading