Skip to content

Commit 268143e

Browse files
authored
Fix Clock implementation (#59)
Re-wire the clock in the node server Add unit and functional tests
1 parent 36c1e81 commit 268143e

File tree

5 files changed

+373
-1
lines changed

5 files changed

+373
-1
lines changed

bin/lib/handlers.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,44 @@ class ContextHandler extends BaseHandler {
4848
return this.wrapResult(result);
4949
}
5050

51+
async handleClock(command, method) {
52+
const context = this.validateResource(this.contexts, command.contextId, 'Context')?.context;
53+
54+
const registry = CommandRegistry.create({
55+
install: async () => {
56+
await context.clock.install(command.options);
57+
return {};
58+
},
59+
fastForward: async () => {
60+
await context.clock.fastForward(command.ticks);
61+
return {};
62+
},
63+
pauseAt: async () => {
64+
await context.clock.pauseAt(command.time);
65+
return {};
66+
},
67+
resume: async () => {
68+
await context.clock.resume();
69+
return {};
70+
},
71+
runFor: async () => {
72+
await context.clock.runFor(command.ticks);
73+
return {};
74+
},
75+
setFixedTime: async () => {
76+
await context.clock.setFixedTime(command.time);
77+
return {};
78+
},
79+
setSystemTime: async () => {
80+
await context.clock.setSystemTime(command.time);
81+
return {};
82+
}
83+
});
84+
85+
const result = await ErrorHandler.safeExecute(() => this.executeWithRegistry(registry, method), { method, contextId: command.contextId });
86+
return this.wrapResult(result);
87+
}
88+
5189
setThrottling(command) {
5290
if (command.throttling && typeof command.throttling === 'object') {
5391
this.contextThrottling.set(command.contextId, {

bin/playwright-server.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ class PlaywrightServer extends BaseHandler {
7373
touchscreen: () => this.interactionHandler.handleTouchscreen(command, actionMethod),
7474
frame: () => this.frameHandler.handle(command, actionMethod),
7575
browserServer: () => this.handleBrowserServer(command, actionMethod),
76-
selectors: () => this.selectorsHandler.handle(command, actionMethod)
76+
selectors: () => this.selectorsHandler.handle(command, actionMethod),
77+
clock: () => this.contextHandler.handleClock(command, actionMethod)
7778
});
7879

7980
if (handlerRegistry.has(actionPrefix)) {

tests/Fixtures/html/clock.html

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<body>
4+
<div id="time"></div>
5+
<button id="btn">Refresh</button>
6+
7+
<section id="long-timeout">
8+
<button id="schedule-long-timeout">Schedule Long Timeout</button>
9+
<div id="long-timeout-status">idle</div>
10+
</section>
11+
12+
<section id="short-timeout">
13+
<button id="schedule-short-timeout">Schedule Short Timeout</button>
14+
<div id="short-timeout-status">idle</div>
15+
</section>
16+
17+
<script>
18+
const time = document.getElementById('time');
19+
const refreshButton = document.getElementById('btn');
20+
function updateTime() {
21+
time.textContent = Date.now().toString();
22+
}
23+
refreshButton.addEventListener('click', updateTime);
24+
updateTime();
25+
26+
const longStatus = document.getElementById('long-timeout-status');
27+
const longButton = document.getElementById('schedule-long-timeout');
28+
let longTimeoutId = null;
29+
longButton.addEventListener('click', () => {
30+
if (longTimeoutId) {
31+
clearTimeout(longTimeoutId);
32+
}
33+
longStatus.textContent = 'waiting';
34+
longTimeoutId = setTimeout(() => {
35+
longStatus.textContent = Date.now().toString();
36+
}, 5000);
37+
});
38+
39+
const shortStatus = document.getElementById('short-timeout-status');
40+
const shortButton = document.getElementById('schedule-short-timeout');
41+
let shortTimeoutId = null;
42+
shortButton.addEventListener('click', () => {
43+
if (shortTimeoutId) {
44+
clearTimeout(shortTimeoutId);
45+
}
46+
shortStatus.textContent = 'waiting';
47+
shortTimeoutId = setTimeout(() => {
48+
shortStatus.textContent = Date.now().toString();
49+
}, 2000);
50+
});
51+
</script>
52+
</body>
53+
</html>
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the community-maintained Playwright PHP project.
7+
* It is not affiliated with or endorsed by Microsoft.
8+
*
9+
* (c) 2025-Present - Playwright PHP - https://github.com/playwright-php
10+
*
11+
* For the full copyright and license information, please view the LICENSE
12+
* file that was distributed with this source code.
13+
*/
14+
15+
namespace Playwright\Tests\Functional\Clock;
16+
17+
use PHPUnit\Framework\Attributes\CoversClass;
18+
use Playwright\Clock\Clock;
19+
use Playwright\Tests\Functional\FunctionalTestCase;
20+
21+
#[CoversClass(Clock::class)]
22+
class ClockTest extends FunctionalTestCase
23+
{
24+
public function testInstallSetsInitialTime(): void
25+
{
26+
$startTime = 1704067200000;
27+
$this->context->clock()->install(['time' => $startTime]);
28+
29+
$this->goto('/clock.html');
30+
31+
$this->assertTimeNear('#time', $startTime, 1000);
32+
}
33+
34+
public function testSetFixedTimeMakesDateNowFixed(): void
35+
{
36+
$startTime = 1704067200000;
37+
$this->context->clock()->install(['time' => 0]);
38+
$this->context->clock()->setFixedTime($startTime);
39+
40+
$this->goto('/clock.html');
41+
42+
$this->expect($this->page->locator('#time'))->toHaveText((string) $startTime);
43+
}
44+
45+
public function testSetSystemTimeChangesDateNow(): void
46+
{
47+
$systemTime = 1704067200000;
48+
$this->context->clock()->install(['time' => 0]);
49+
50+
$this->goto('/clock.html');
51+
52+
$this->context->clock()->setSystemTime($systemTime);
53+
$this->page->click('#btn');
54+
55+
$this->assertTimeNear('#time', $systemTime, 1000);
56+
}
57+
58+
public function testPauseAtPausesClock(): void
59+
{
60+
$startTime = 1_600_000_000_000;
61+
$this->context->clock()->install(['time' => $startTime]);
62+
63+
$this->goto('/clock.html');
64+
65+
$targetTime = $startTime + 5_000;
66+
$this->context->clock()->pauseAt($targetTime);
67+
$this->page->click('#btn');
68+
69+
$this->expect($this->page->locator('#time'))->toHaveText((string) $targetTime);
70+
}
71+
72+
public function testFastForwardAdvancesTimers(): void
73+
{
74+
$startTime = 1_600_000_000_000;
75+
$this->context->clock()->install(['time' => $startTime]);
76+
77+
$this->goto('/clock.html');
78+
$this->page->click('#schedule-long-timeout');
79+
$this->expect($this->page->locator('#long-timeout-status'))->toContainText('waiting');
80+
81+
$this->context->clock()->fastForward(5000);
82+
83+
$this->assertTimeNear('#long-timeout-status', $startTime + 5000, 200);
84+
}
85+
86+
public function testRunForAdvancesTimers(): void
87+
{
88+
$startTime = 1_600_000_000_000;
89+
$this->context->clock()->install(['time' => $startTime]);
90+
91+
$this->goto('/clock.html');
92+
$this->page->click('#schedule-short-timeout');
93+
$this->expect($this->page->locator('#short-timeout-status'))->toContainText('waiting');
94+
95+
$this->context->clock()->runFor(2000);
96+
97+
$this->assertTimeNear('#short-timeout-status', $startTime + 2000, 200);
98+
}
99+
100+
public function testResumeResumesFromPause(): void
101+
{
102+
$startTime = 1_600_000_000_000;
103+
$this->context->clock()->install(['time' => $startTime]);
104+
105+
$this->goto('/clock.html');
106+
$status = $this->page->locator('#short-timeout-status');
107+
$this->page->click('#schedule-short-timeout');
108+
$this->expect($status)->toHaveText('waiting');
109+
110+
$this->context->clock()->pauseAt($startTime + 1000);
111+
$this->context->clock()->runFor(500);
112+
$this->expect($status)->toHaveText('waiting');
113+
114+
$this->context->clock()->resume();
115+
$this->context->clock()->runFor(1000);
116+
117+
$this->assertTimeNear('#short-timeout-status', $startTime + 2000, 300);
118+
}
119+
120+
private function assertTimeNear(string $selector, int $expected, int $tolerance = 200): void
121+
{
122+
$text = $this->page->locator($selector)->textContent();
123+
self::assertIsString($text, sprintf('Element %s did not contain time text', $selector));
124+
125+
$actual = (int) trim($text);
126+
$diff = abs($actual - $expected);
127+
128+
self::assertLessThanOrEqual(
129+
$tolerance,
130+
$diff,
131+
sprintf('Expected %s time near %d (±%d), got %d (diff %d)', $selector, $expected, $tolerance, $actual, $diff)
132+
);
133+
}
134+
}

tests/Unit/Clock/ClockTest.php

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the community-maintained Playwright PHP project.
7+
* It is not affiliated with or endorsed by Microsoft.
8+
*
9+
* (c) 2025-Present - Playwright PHP - https://github.com/playwright-php
10+
*
11+
* For the full copyright and license information, please view the LICENSE
12+
* file that was distributed with this source code.
13+
*/
14+
15+
namespace Playwright\Tests\Unit\Clock;
16+
17+
use PHPUnit\Framework\Attributes\CoversClass;
18+
use PHPUnit\Framework\TestCase;
19+
use Playwright\Clock\Clock;
20+
use Playwright\Transport\MockTransport;
21+
22+
#[CoversClass(Clock::class)]
23+
final class ClockTest extends TestCase
24+
{
25+
public function testFastForwardSendsIntegerTicks(): void
26+
{
27+
$transport = new MockTransport();
28+
$transport->connect();
29+
$transport->queueResponse([]);
30+
31+
$clock = new Clock($transport, 'ctx_ff_1');
32+
$clock->fastForward(5000);
33+
34+
$sent = $transport->getSentMessages();
35+
$this->assertCount(1, $sent);
36+
$this->assertSame('clock.fastForward', $sent[0]['action']);
37+
$this->assertSame('ctx_ff_1', $sent[0]['contextId']);
38+
$this->assertSame(5000, $sent[0]['ticks']);
39+
}
40+
41+
public function testFastForwardSendsStringTicks(): void
42+
{
43+
$transport = new MockTransport();
44+
$transport->connect();
45+
$transport->queueResponse([]);
46+
47+
$clock = new Clock($transport, 'ctx_ff_2');
48+
$clock->fastForward('01:00:00');
49+
50+
$sent = $transport->getSentMessages();
51+
$this->assertSame('clock.fastForward', $sent[0]['action']);
52+
$this->assertSame('01:00:00', $sent[0]['ticks']);
53+
}
54+
55+
public function testInstallForwardsOptionsIncludingDateTime(): void
56+
{
57+
$transport = new MockTransport();
58+
$transport->connect();
59+
$transport->queueResponse([]);
60+
61+
$clock = new Clock($transport, 'ctx_install_1');
62+
$dt = new \DateTimeImmutable('2025-01-01T00:00:00Z');
63+
64+
$clock->install(['time' => $dt]);
65+
66+
$sent = $transport->getSentMessages();
67+
$this->assertSame('clock.install', $sent[0]['action']);
68+
$this->assertArrayHasKey('options', $sent[0]);
69+
$this->assertSame($dt, $sent[0]['options']['time']);
70+
}
71+
72+
public function testPauseAtConvertsDateTimeToMilliseconds(): void
73+
{
74+
$transport = new MockTransport();
75+
$transport->connect();
76+
$transport->queueResponse([]);
77+
78+
$clock = new Clock($transport, 'ctx_pause_1');
79+
$dt = new \DateTimeImmutable('2025-02-02T12:34:56Z');
80+
81+
$clock->pauseAt($dt);
82+
83+
$sent = $transport->getSentMessages();
84+
$this->assertSame('clock.pauseAt', $sent[0]['action']);
85+
$this->assertSame($dt->getTimestamp() * 1000, $sent[0]['time']);
86+
}
87+
88+
public function testResumeSendsMessage(): void
89+
{
90+
$transport = new MockTransport();
91+
$transport->connect();
92+
$transport->queueResponse([]);
93+
94+
$clock = new Clock($transport, 'ctx_resume_1');
95+
$clock->resume();
96+
97+
$sent = $transport->getSentMessages();
98+
$this->assertSame('clock.resume', $sent[0]['action']);
99+
}
100+
101+
public function testRunForSendsTicks(): void
102+
{
103+
$transport = new MockTransport();
104+
$transport->connect();
105+
$transport->queueResponse([]);
106+
107+
$clock = new Clock($transport, 'ctx_runfor_1');
108+
$clock->runFor(1234);
109+
110+
$sent = $transport->getSentMessages();
111+
$this->assertSame('clock.runFor', $sent[0]['action']);
112+
$this->assertSame(1234, $sent[0]['ticks']);
113+
}
114+
115+
public function testSetFixedTimeConvertsDateTimeToMilliseconds(): void
116+
{
117+
$transport = new MockTransport();
118+
$transport->connect();
119+
$transport->queueResponse([]);
120+
121+
$clock = new Clock($transport, 'ctx_fixed_1');
122+
$dt = new \DateTimeImmutable('2025-03-03T00:00:00Z');
123+
124+
$clock->setFixedTime($dt);
125+
126+
$sent = $transport->getSentMessages();
127+
$this->assertSame('clock.setFixedTime', $sent[0]['action']);
128+
$this->assertSame($dt->getTimestamp() * 1000, $sent[0]['time']);
129+
}
130+
131+
public function testSetSystemTimeConvertsDateTimeToMilliseconds(): void
132+
{
133+
$transport = new MockTransport();
134+
$transport->connect();
135+
$transport->queueResponse([]);
136+
137+
$clock = new Clock($transport, 'ctx_system_1');
138+
$dt = new \DateTimeImmutable('2025-04-04T00:00:00Z');
139+
140+
$clock->setSystemTime($dt);
141+
142+
$sent = $transport->getSentMessages();
143+
$this->assertSame('clock.setSystemTime', $sent[0]['action']);
144+
$this->assertSame($dt->getTimestamp() * 1000, $sent[0]['time']);
145+
}
146+
}

0 commit comments

Comments
 (0)