Skip to content
Open
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
19 changes: 18 additions & 1 deletion bin/lib/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class ContextHandler extends BaseHandler {
setOffline: () => context.setOffline(!!command.offline),
setGeolocation: () => context.setGeolocation(command.geolocation),
addCookies: () => context.addCookies(command.cookies),
clearCookies: () => context.clearCookies(),
clearCookies: () => context.clearCookies(command.options || {}),
grantPermissions: () => context.grantPermissions(command.permissions, command.origin ? { origin: command.origin } : undefined),
clearPermissions: () => context.clearPermissions(),
startTracing: () => context.tracing.start(command.options || {}),
Expand Down Expand Up @@ -222,6 +222,7 @@ class PageHandler extends BaseHandler {
viewportSize: () => this.createValueResult(page.viewportSize()),
waitForURL: () => page.waitForURL(command.url, command.options),
waitForSelector: () => page.waitForSelector(command.selector, command.options),
waitForFunction: () => this.waitForFunction(page, command),
screenshot: () => PromiseUtils.wrapBinary(page.screenshot(command.options)),
pdf: () => PromiseUtils.wrapBinary(page.pdf(command.options || {})),
evaluateHandle: () => this.evaluateHandle(page, command),
Expand Down Expand Up @@ -272,6 +273,22 @@ class PageHandler extends BaseHandler {
}
}

async waitForFunction(page, command) {
try {
let func;
try {
func = eval(`(${command.pageFunction})`);
} catch (e) {
func = command.pageFunction;
}

await page.waitForFunction(func, command.arg, command.options);
} catch (error) {
logger.error('PAGE WAITFORFUNCTION ERROR', { message: error.message });
throw error;
}
}

async waitForResponse(page, command) {
const jsAction = command.jsAction;
const [response] = await Promise.all([
Expand Down
2 changes: 1 addition & 1 deletion src/Browser/BrowserContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ public function loadStorageState(string $filePath): void
$this->setStorageState($storageState);
}

public function setGeolocation(?float $latitude, ?float $longitude, ?float $accuracy = null): void
public function setGeolocation(?float $latitude, ?float $longitude, ?float $accuracy = 0): void
{
$this->transport->send([
'action' => 'context.setGeolocation',
Expand Down
64 changes: 64 additions & 0 deletions src/Page/Options/WaitForFunctionOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?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\Page\Options;

final class WaitForFunctionOptions
{
/**
* @param float|null $timeout Timeout in ms
* @param float|'raf'|null $polling Polling-Intervall in ms oder 'raf'
*/
public function __construct(
public ?float $timeout = null,
public float|string|null $polling = null,
) {
}

/**
* @return array<string, mixed>
*/
public function toArray(): array
{
$options = [];

if (null !== $this->timeout) {
$options['timeout'] = $this->timeout;
}

if (null !== $this->polling) {
$options['polling'] = $this->polling;
}

return $options;
}

/**
* @param array<string, mixed>|self $options
*/
public static function from(array|self $options = []): self
{
if ($options instanceof self) {
return $options;
}

/** @var float|null $timeout */
$timeout = $options['timeout'] ?? null;

/** @var float|'raf'|null $polling */
$polling = $options['polling'] ?? null;

return new self($timeout, $polling);
}
}
14 changes: 14 additions & 0 deletions src/Page/Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
use Playwright\Page\Options\SetInputFilesOptions;
use Playwright\Page\Options\StyleTagOptions;
use Playwright\Page\Options\TypeOptions;
use Playwright\Page\Options\WaitForFunctionOptions;
use Playwright\Page\Options\WaitForLoadStateOptions;
use Playwright\Page\Options\WaitForPopupOptions;
use Playwright\Page\Options\WaitForResponseOptions;
Expand Down Expand Up @@ -836,6 +837,19 @@ public function waitForLoadState(string $state = 'load', array|WaitForLoadStateO
return $this;
}

/**
* @param array<string, mixed>|WaitForFunctionOptions $options
*/
public function waitForFunction(string $pageFunction, mixed $arg = null, array|WaitForFunctionOptions $options = []): self
{
$options = WaitForFunctionOptions::from($options)->toArray();

$normalized = self::normalizeForPage($pageFunction);
$this->sendCommand('waitForFunction', ['pageFunction' => $normalized, 'arg' => $arg, 'options' => $options]);

return $this;
}

/**
* @param array<string, mixed>|WaitForUrlOptions $options
*/
Expand Down
6 changes: 6 additions & 0 deletions src/Page/PageInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use Playwright\Page\Options\SetInputFilesOptions;
use Playwright\Page\Options\StyleTagOptions;
use Playwright\Page\Options\TypeOptions;
use Playwright\Page\Options\WaitForFunctionOptions;
use Playwright\Page\Options\WaitForLoadStateOptions;
use Playwright\Page\Options\WaitForPopupOptions;
use Playwright\Page\Options\WaitForResponseOptions;
Expand Down Expand Up @@ -194,6 +195,11 @@ public function setDefaultTimeout(int $timeout): self;
*/
public function waitForLoadState(string $state = 'load', array|WaitForLoadStateOptions $options = []): self;

/**
* @param array<string, mixed>|WaitForFunctionOptions $options
*/
public function waitForFunction(string $pageFunction, mixed $arg = null, array|WaitForFunctionOptions $options = []): self;

/**
* @param string|callable $url
* @param array<string, mixed>|WaitForUrlOptions $options
Expand Down
20 changes: 3 additions & 17 deletions tests/Integration/Browser/BrowserContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,23 +170,9 @@ public function itSetsGeolocation(): void

$page->click('button');

// Poll for either coordinates or error text since Page::waitForFunction is not available
$deadline = microtime(true) + 5.0; // 5 seconds
do {
$content = $page->content() ?? '';
$hasCoordinates = str_contains($content, '59.95,30.31667');
$hasError = str_contains($content, 'Error');
if ($hasCoordinates || $hasError) {
break;
}
usleep(100 * 1000); // 100ms
} while (microtime(true) < $deadline);

$content = $page->content();
$hasCoordinates = str_contains($content, '59.95,30.31667');
$hasError = str_contains($content, 'Error:');

$this->assertTrue($hasCoordinates || $hasError, 'Geolocation API should respond with either coordinates or error message');
$page->waitForFunction("() => document.body.innerText.includes('59.95') || document.body.innerText.includes('Error: ')", options: ['timeout' => 200]);

$this->assertStringContainsString('59.95,30.31667', $page->content(), 'Geolocation API should respond with either coordinates or error message');

$page->close();
}
Expand Down
87 changes: 87 additions & 0 deletions tests/Integration/Page/PageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,91 @@ public function itWaitsForLoadState(): void

$this->assertStringContainsString('/page2.html', $this->page->url());
}

#[Test]
public function itWaitsForFunction(): void
{
$this->page->setViewportSize(500, 500);

$this->page->waitForFunction(
'window.innerWidth < 600',
null,
['timeout' => 100, 'polling' => 50]
);

$this->assertSame(['width' => 500, 'height' => 500], $this->page->viewportSize());
}

#[Test]
public function itWaitsForFunctionSetTimeout(): void
{
$this->page->setContent('<div id="status">loading</div>');
$this->page->evaluate('() => setTimeout(() => { document.getElementById("status").textContent = "ready"; }, 200)');

$this->page->waitForFunction(
'document.querySelector("#status").textContent === "ready"',
null,
['timeout' => 300, 'polling' => 50]
);

$this->assertSame('ready', $this->page->evaluate('() => document.querySelector("#status").textContent'));
}

#[Test]
public function itWaitsForFunctionWithRafPolling(): void
{
$this->page->setContent('<div id="x">0</div><script>requestAnimationFrame(() => { document.getElementById("x").textContent = "1"; });</script>');

$this->page->waitForFunction(
'() => document.querySelector("#x") && document.querySelector("#x").textContent === "1"',
null,
['timeout' => 500, 'polling' => 'raf']
);

$this->assertSame('1', $this->page->evaluate('() => document.querySelector("#x").textContent'));
}

#[Test]
public function itWaitsForFunctionWithArgument(): void
{
$this->page->setContent('<div id="status">loading</div>');
$this->page->evaluate('() => setTimeout(() => { document.getElementById("status").textContent = "ready"; }, 100)');

$this->page->waitForFunction(
'arg => {
const el = document.getElementById(arg.selector);
return !!el && el.textContent === arg.text;
}',
['selector' => 'status', 'text' => 'ready'],
['timeout' => 300, 'polling' => 50]
);

$this->assertSame('ready', $this->page->evaluate('() => document.querySelector("#status").textContent'));
}

#[Test]
public function itThrowsExceptionOnWaitForFunctionTimeout(): void
{
$this->expectException(\Playwright\Exception\TimeoutException::class);
$this->expectExceptionMessage('page.waitForFunction: Timeout 100ms exceeded.');

$this->page->waitForFunction(
'() => false',
null,
['timeout' => 100]
);
}

#[Test]
public function itThrowsExceptionOnInvalidWaitForFunctionPolling(): void
{
$this->expectException(\Playwright\Exception\PlaywrightException::class);
$this->expectExceptionMessage('Unknown polling option: invalid');

$this->page->waitForFunction(
'() => true',
null,
['polling' => 'invalid']
);
}
}
77 changes: 77 additions & 0 deletions tests/Unit/Page/Options/WaitForFunctionOptionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?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\Tests\Unit\Page\Options;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Playwright\Page\Options\WaitForFunctionOptions;

#[CoversClass(WaitForFunctionOptions::class)]
final class WaitForFunctionOptionsTest extends TestCase
{
public function testItCreatesFromObject(): void
{
$options = new WaitForFunctionOptions(timeout: 5000.0, polling: 100.0);
$this->assertSame($options, WaitForFunctionOptions::from($options));
}

public function testItCreatesFromArray(): void
{
$options = WaitForFunctionOptions::from(['timeout' => 5000.0, 'polling' => 100.0]);
$this->assertSame(5000.0, $options->timeout);
$this->assertSame(100.0, $options->polling);
}

public function testItCreatesFromArrayWithRaf(): void
{
$options = WaitForFunctionOptions::from(['polling' => 'raf']);
$this->assertSame('raf', $options->polling);
}

public function testItThrowsExceptionForInvalidInput(): void
{
$this->expectException(\TypeError::class);

WaitForFunctionOptions::from('invalid');
}

public function testToReturnArray(): void
{
$options = new WaitForFunctionOptions(
timeout: 5000.0,
polling: 100.0
);

$expected = [
'timeout' => 5000.0,
'polling' => 100.0,
];

$this->assertSame($expected, $options->toArray());
}

public function testToReturnArrayWithRaf(): void
{
$options = new WaitForFunctionOptions(
polling: 'raf'
);

$expected = [
'polling' => 'raf',
];

$this->assertSame($expected, $options->toArray());
}
}
Loading