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: 5 additions & 1 deletion bin/lib/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,11 @@ class ContextHandler extends BaseHandler {
}

async handleTracing(command, method) {
const context = this.contexts.get(command.contextId)?.context ?? this.apiContexts.get(command.contextId);
// API requests share the browser context ID, but must target request.tracing.
const browserContext = this.contexts.get(command.contextId)?.context;
const context = browserContext
? (command.apiRequest ? browserContext.request : browserContext)
: this.apiContexts.get(command.contextId);

if (!context) {
throw new Error(`Tracing context not found: ${command.contextId}`);
Expand Down
2 changes: 1 addition & 1 deletion src/API/APIRequestContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public function storageState(?string $path = null): array

public function tracing(): TracingInterface
{
return new Tracing($this->transport, $this->contextId);
return new Tracing($this->transport, $this->contextId, apiRequest: true);
}

public function dispose(): void
Expand Down
29 changes: 21 additions & 8 deletions src/Tracing/Tracing.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ final class Tracing implements TracingInterface
public function __construct(
private readonly TransportInterface $transport,
private readonly string $contextId,
private readonly bool $apiRequest = false,
) {
}

public function start(array|StartOptions $options = []): void
{
$options = StartOptions::from($options);
$this->transport->send([
$this->send([
'action' => 'tracingStart',
'contextId' => $this->contextId,
'options' => $options->toArray(),
Expand All @@ -42,7 +43,7 @@ public function start(array|StartOptions $options = []): void
public function startChunk(array|StartChunkOptions $options = []): void
{
$options = StartChunkOptions::from($options);
$this->transport->send([
$this->send([
'action' => 'tracingStartChunk',
'contextId' => $this->contextId,
'options' => $options->toArray(),
Expand All @@ -52,7 +53,7 @@ public function startChunk(array|StartChunkOptions $options = []): void
public function stop(array|StopOptions $options = []): void
{
$options = StopOptions::from($options);
$this->transport->send([
$this->send([
'action' => 'tracingStop',
'contextId' => $this->contextId,
'options' => $options->toArray(),
Expand All @@ -62,7 +63,7 @@ public function stop(array|StopOptions $options = []): void
public function stopChunk(array|StopChunkOptions $options = []): void
{
$options = StopChunkOptions::from($options);
$this->transport->send([
$this->send([
'action' => 'tracingStopChunk',
'contextId' => $this->contextId,
'options' => $options->toArray(),
Expand All @@ -72,7 +73,7 @@ public function stopChunk(array|StopChunkOptions $options = []): void
public function startHar(string $path, array|StartHarOptions $options = []): void
{
$options = StartHarOptions::from($options);
$this->transport->send([
$this->send([
'action' => 'tracingStartHar',
'contextId' => $this->contextId,
'path' => $path,
Expand All @@ -82,7 +83,7 @@ public function startHar(string $path, array|StartHarOptions $options = []): voi

public function stopHar(): void
{
$this->transport->send([
$this->send([
'action' => 'tracingStopHar',
'contextId' => $this->contextId,
]);
Expand All @@ -99,14 +100,26 @@ public function group(string $name, ?string $location = null): void
$payload['location'] = $location;
}

$this->transport->send($payload);
$this->send($payload);
}

public function groupEnd(): void
{
$this->transport->send([
$this->send([
'action' => 'tracingGroupEnd',
'contextId' => $this->contextId,
]);
}

/**
* @param array<string, mixed> $message
*/
private function send(array $message): void
{
if ($this->apiRequest) {
$message['apiRequest'] = true;
}

$this->transport->send($message);
}
}
81 changes: 81 additions & 0 deletions tests/Functional/Tracing/TracingApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@
namespace Playwright\Tests\Functional\Tracing;

use PHPUnit\Framework\Attributes\CoversClass;
use Playwright\API\APIRequest;
use Playwright\API\APIRequestContext;
use Playwright\Browser\BrowserContext;
use Playwright\Configuration\PlaywrightConfig;
use Playwright\Regex;
use Playwright\Tests\Functional\FunctionalTestCase;
use Playwright\Tracing\Options\StartHarOptions;
use Playwright\Tracing\Tracing;
use Playwright\Transport\TransportFactory;
use Psr\Log\NullLogger;

#[CoversClass(Tracing::class)]
#[CoversClass(StartHarOptions::class)]
Expand Down Expand Up @@ -155,11 +159,87 @@ public function testHarRecordingIsAvailableOnTheApiRequestContext(): void
$this->assertStringContainsString('/index.html', json_encode($entries, \JSON_THROW_ON_ERROR));
}

public function testStandaloneApiHarRecordingCapturesRequests(): void
{
$transport = (new TransportFactory())->create(new PlaywrightConfig(), new NullLogger());
$transport->connect();

try {
$request = (new APIRequest($transport))->newContext();
try {
$harPath = $this->tempDir.'/standalone.har';
$tracing = $request->tracing();
$tracing->startHar($harPath);
$response = $request->get($this->getBaseUrl().'/index.html');
$tracing->stopHar();

$this->assertSame(200, $response->status());
$this->assertStringContainsString('/index.html', json_encode($this->readHarEntries($harPath), \JSON_THROW_ON_ERROR));
} finally {
$request->dispose();
}
} finally {
$transport->disconnect();
}
}

public function testBrowserAndApiHarRecordingsKeepTheirRequests(): void
{
$browserHar = $this->tempDir.'/browser.har';
$apiHar = $this->tempDir.'/request.har';
$browserTracing = $this->context->tracing();
$request = $this->context->request();
$apiTracing = $request->tracing();

$browserTracing->startHar($browserHar);
$this->goto('/index.html');
$browserTracing->stopHar();

$apiTracing->startHar($apiHar);
$request->get($this->getBaseUrl().'/api/echo');
$apiTracing->stopHar();

$this->assertContains($this->getBaseUrl().'/api/echo', array_column(array_column($this->readHarEntries($apiHar), 'request'), 'url'));
$this->assertContains($this->getBaseUrl().'/index.html', array_column(array_column($this->readHarEntries($browserHar), 'request'), 'url'));
}

public function testApiTraceGroupsAndChunksKeepTheirEvents(): void
{
$browserPath = $this->tempDir.'/browser-trace.zip';
$apiPath = $this->tempDir.'/api-chunk.zip';
$browserTracing = $this->context->tracing();
$request = $this->context->request();
$apiTracing = $request->tracing();

$apiTracing->start();
$apiTracing->startChunk(['title' => 'API chunk']);
$apiTracing->group('api-only-step');
$request->get($this->getBaseUrl().'/api/echo');
$apiTracing->groupEnd();
$apiTracing->stopChunk(['path' => $apiPath]);
$apiTracing->stop();

$browserTracing->start();
$browserTracing->group('browser-only-step');
$this->goto('/index.html');
$browserTracing->groupEnd();
$browserTracing->stop(['path' => $browserPath]);

$apiEvents = $this->readTraceEvents($apiPath);
$browserEvents = $this->readTraceEvents($browserPath);
$this->assertStringContainsString('api-only-step', $apiEvents);
$this->assertStringContainsString('/api/echo', $apiEvents);
$this->assertStringNotContainsString('browser-only-step', $apiEvents);
$this->assertStringContainsString('browser-only-step', $browserEvents);
$this->assertStringNotContainsString('api-only-step', $browserEvents);
}

/**
* @return array<int, mixed>
*/
private function readHarEntries(string $harPath): array
{
$this->assertFileExists($harPath);
$raw = file_get_contents($harPath);
$this->assertIsString($raw);

Expand All @@ -175,6 +255,7 @@ private function readHarEntries(string $harPath): array

private function readTraceEvents(string $zipPath): string
{
$this->assertFileExists($zipPath);
$zip = new \ZipArchive();
$this->assertTrue($zip->open($zipPath));

Expand Down
3 changes: 2 additions & 1 deletion tests/Unit/API/APIRequestContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ public function testTracingReturnsATracingInstance(): void
$this->assertInstanceOf(TracingInterface::class, $this->context->tracing());
}

public function testTracingTargetsTheSameContext(): void
public function testTracingTargetsTheApiRequestContext(): void
{
$this->transport->expects($this->once())
->method('send')
Expand All @@ -234,6 +234,7 @@ public function testTracingTargetsTheSameContext(): void
'contextId' => 'context_1',
'path' => '/tmp/api.har',
'options' => [],
'apiRequest' => true,
])
->willReturn([]);

Expand Down
34 changes: 34 additions & 0 deletions tests/Unit/Tracing/TracingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
namespace Playwright\Tests\Unit\Tracing;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Playwright\Tracing\Tracing;
use Playwright\Transport\TransportInterface;
Expand Down Expand Up @@ -143,4 +144,37 @@ public function testGroupEndSendsAction(): void

$this->tracing->groupEnd();
}

/**
* @param list<mixed> $arguments
*/
#[DataProvider('apiTracingOperations')]
public function testEveryApiTracingOperationKeepsItsTarget(string $method, array $arguments, string $action): void
{
$this->transport->expects($this->once())
->method('send')
->with($this->callback(static fn (array $message): bool => $action === $message['action']
&& 'context_1' === $message['contextId']
&& true === ($message['apiRequest'] ?? false)
))
->willReturn([]);

$tracing = new Tracing($this->transport, 'context_1', apiRequest: true);
$tracing->$method(...$arguments);
}

/**
* @return iterable<string, array{string, list<mixed>, string}>
*/
public static function apiTracingOperations(): iterable
{
yield 'start' => ['start', [], 'tracingStart'];
yield 'start chunk' => ['startChunk', [], 'tracingStartChunk'];
yield 'stop' => ['stop', [], 'tracingStop'];
yield 'stop chunk' => ['stopChunk', [], 'tracingStopChunk'];
yield 'start HAR' => ['startHar', ['/tmp/api.har'], 'tracingStartHar'];
yield 'stop HAR' => ['stopHar', [], 'tracingStopHar'];
yield 'group' => ['group', ['API call'], 'tracingGroup'];
yield 'group end' => ['groupEnd', [], 'tracingGroupEnd'];
}
}
Loading